From 396dd5274b2ad7b44d8fddb60a203fb7dd5e175b Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Tue, 21 Jul 2015 09:07:45 +0000 Subject: [PATCH 001/115] Add part attribute from gird into dtd generator --- etemplate/js/widget_browser.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/etemplate/js/widget_browser.js b/etemplate/js/widget_browser.js index 703aa818e4..62abc4da94 100644 --- a/etemplate/js/widget_browser.js +++ b/etemplate/js/widget_browser.js @@ -352,13 +352,15 @@ widget_browser.prototype._dtd_widgets = function (_type, _widget) \n\ + width CDATA #IMPLIED>\n\ + \n\ \n\ \r\n'; break; From 6001f9e189bdeaab1762fe87629cb9800651f7c3 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Tue, 21 Jul 2015 09:09:50 +0000 Subject: [PATCH 002/115] Updates etemplate2.dtd file including latest changes of widgets --- etemplate/doc/etemplate2.dtd | 5541 ++++++++++++++++++++-------------- 1 file changed, 3204 insertions(+), 2337 deletions(-) diff --git a/etemplate/doc/etemplate2.dtd b/etemplate/doc/etemplate2.dtd index 62a4576cbe..43234a57b3 100644 --- a/etemplate/doc/etemplate2.dtd +++ b/etemplate/doc/etemplate2.dtd @@ -1,4 +1,4 @@ - + - - -" > + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 63ea85ffd8bed94e0c57a126c262f64b5e278144 Mon Sep 17 00:00:00 2001 From: Klaus Leithoff Date: Tue, 21 Jul 2015 11:54:15 +0000 Subject: [PATCH 003/115] use SYNC_GAL Keys for GAL search --- .../inc/class.addressbook_zpush.inc.php | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/addressbook/inc/class.addressbook_zpush.inc.php b/addressbook/inc/class.addressbook_zpush.inc.php index c651a0599e..709246c945 100644 --- a/addressbook/inc/class.addressbook_zpush.inc.php +++ b/addressbook/inc/class.addressbook_zpush.inc.php @@ -795,25 +795,30 @@ class addressbook_zpush implements activesync_plugin_write, activesync_plugin_se } //error_log(__METHOD__.'('.array2string($searchquery).') range='.array2string($range)); - $items = array(); - if (($contacts =& $this->addressbook->search($searchquery['query'], false, false, '', '%', false, 'OR', $range))) + $items = $filter = array(); + $filter['cols_to_search'] = array('n_fn', 'n_family', 'n_given', + 'room','org_name', 'title', 'role', 'tel_work', 'tel_home', 'tel_cell', + 'email', 'email_home'); + if (($contacts =& $this->addressbook->search($searchquery['query'], false, false, '', '%', false, 'OR', $range, $filter))) { foreach($contacts as $contact) { - $item['username'] = $contact['n_family']; - $item['fullname'] = $contact['n_fn']; - if (!trim($item['fullname'])) $item['fullname'] = $item['username']; - $item['emailaddress'] = $contact['email'] ? $contact['email'] : (string)$contact['email_private'] ; - $item['nameid'] = $searchquery; - $item['phone'] = (string)$contact['tel_work']; - $item['homephone'] = (string)$contact['tel_home']; - $item['mobilephone'] = (string)$contact['tel_cell']; - $item['company'] = (string)$contact['org_name']; - $item['office'] = $contact['room']; - $item['title'] = $contact['title']; + $item[SYNC_GAL_ALIAS] = $contact['contact_id']; + $item[SYNC_GAL_LASTNAME] = $contact['n_family']; + $item[SYNC_GAL_FIRSTNAME] = $contact['n_given']; + $item[SYNC_GAL_DISPLAYNAME] = $contact['n_fn']; + if (!trim($item[SYNC_GAL_DISPLAYNAME])) $item[SYNC_GAL_DISPLAYNAME] = $contact['n_family']?$contact['n_family']:$contact['org_name']; + $item[SYNC_GAL_EMAILADDRESS] = $contact['email'] ? $contact['email'] : (string)$contact['email_private'] ; + //$item['nameid'] = $searchquery; + $item[SYNC_GAL_PHONE] = (string)$contact['tel_work']; + $item[SYNC_GAL_HOMEPHONE] = (string)$contact['tel_home']; + $item[SYNC_GAL_MOBILEPHONE] = (string)$contact['tel_cell']; + $item[SYNC_GAL_COMPANY] = (string)$contact['org_name']; + $item[SYNC_GAL_OFFICE] = $contact['room']; + $item[SYNC_GAL_TITLE ] = $contact['title']; //do not return users without email - if (!trim($item['emailaddress'])) continue; + if (!trim($item[SYNC_GAL_EMAILADDRESS])) continue; $items[] = $item; } From be5421f1c7b4ac453acb737d72b718c2f139713c Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 21 Jul 2015 12:53:23 +0000 Subject: [PATCH 004/115] catch SQL error "Illegal mix of collations" caused by non-ascii chars compared with ascii field uid --- addressbook/inc/class.addressbook_sql.inc.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/addressbook/inc/class.addressbook_sql.inc.php b/addressbook/inc/class.addressbook_sql.inc.php index 911d333377..51ab7830c2 100644 --- a/addressbook/inc/class.addressbook_sql.inc.php +++ b/addressbook/inc/class.addressbook_sql.inc.php @@ -740,7 +740,15 @@ class addressbook_sql extends so_sql_cf { $keys = array('uid' => $keys); } - $contact = parent::read($keys,$extra_cols,$join); + try { + $contact = parent::read($keys,$extra_cols,$join); + } + // catch Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' (1267) + // caused by non-ascii chars compared with ascii field uid + catch(egw_exception_db $e) { + _egw_log_exception($e); + return false; + } // enforce a minium uid strength if (is_array($contact) && (!isset($contact['uid']) From 27634fd18d27251d29f0186f29f163fcf7db4b7f Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Tue, 21 Jul 2015 13:24:25 +0000 Subject: [PATCH 005/115] Some more W.I.P. of mail tree --- mail/inc/class.mail_tree.inc.php | 130 ++++++++++++++++++++++++------- 1 file changed, 103 insertions(+), 27 deletions(-) diff --git a/mail/inc/class.mail_tree.inc.php b/mail/inc/class.mail_tree.inc.php index 87eadc9e4e..cfbeef506c 100644 --- a/mail/inc/class.mail_tree.inc.php +++ b/mail/inc/class.mail_tree.inc.php @@ -32,7 +32,26 @@ class mail_tree */ static $delimiter = '::'; + /** + * Icons used for nodes different states + * + * @var array + */ + static $leafImages = array( + 'folderNoSelectClosed' => "folderNoSelectClosed.gif", + 'folderNoSelectOpen' => "folderNoSelectOpen.gif", + 'folderOpen' => "folderOpen.gif", + 'folderClosed' => "MailFolderClosed.png", + 'folderLeaf' => "MailFolderPlain.png", + 'folderHome' => "kfm_home.png", + 'folderAccount' => "thunderbird.png", + ); + /** + * Mail tree constructor + * + * @param object $mail_ui + */ function __construct($mail_ui) { $this->ui = $mail_ui; } @@ -53,9 +72,9 @@ class mail_tree 'id' => $_profileID.self::$delimiter.'INBOX', 'text' => $_err, 'tooltip' => $_err, - 'im0' => "folderNoSelectClosed.gif", - 'im1' => "folderNoSelectOpen.gif", - 'im2' => "folderNoSelectClosed.gif", + 'im0' => self::$leafImages["folderNoSelectClosed"], + 'im1' => self::$leafImages["folderNoSelectOpen.gif"], + 'im2' => self::$leafImages["folderNoSelectClosed"], 'path'=> $_path, 'parent' => $_parent ); @@ -66,12 +85,32 @@ class mail_tree 'item'=> array( 'text'=>'INBOX', 'tooltip'=>'INBOX'.' '.lang('(not connected)'), - 'im0'=>'kfm_home.png' + 'im0'=> self::$leafImages['folderHome'] ) ) ); } + /** + * Get folder data from path + * + * @param string $path a node path + * @return array returns an array of data extracted from given node path + */ + static function getFolderData ($_path, $_hDelimiter) + { + list(,$path) = explode(self::$delimiter, $_path); + $parts = explode($_hDelimiter, $path); + $name = array_pop($parts); + return array ( + 'name' => $name, + 'mailbox' => $path, + 'parent' => implode($_hDelimiter, $parts), + 'text' => $name, + 'tooltip' => $name + ); + } + /** * getTree provides tree structure regarding to selected node * @@ -83,7 +122,7 @@ class mail_tree { //Init mail folders $tree = array(tree::ID=> $_parent?$_parent:0,tree::CHILDREN => array()); - + $hDelimiter = $this->ui->mail_bo->getHierarchyDelimiter(); $fn_nodeHasChildren = function ($_node) { $hasChildren = 0; @@ -93,7 +132,8 @@ class mail_tree return $hasChildren; }; - if ($_parent) $_profileID = $this->ui->icServerID; + if ($_parent) $_profileID = $this->ui->mail_bo->icServerID; + if (is_numeric($_profileID) && $_profileID != $this->ui->mail_bo->profileID) { try @@ -104,11 +144,12 @@ class mail_tree } } - if ($_parent) + if ($_parent) // Single node loader { try { - $folders = $this->ui->mail_bo->getFolderArray($_parent); + $nodeInfo = self::getFolderData($_parent, $hDelimiter); + $folders = $this->ui->mail_bo->getFolderArray($nodeInfo['mailbox']); } catch (Exception $ex) { return self::treeLeafNoConnectionArray($_profileID, $ex->getMessage(),array($_profileID), ''); } @@ -117,17 +158,21 @@ class mail_tree foreach ($folders as &$node) { $nodeId = $_profileID.self::$delimiter.$node['MAILBOX']; - + $nodeData = self::getFolderData($nodeId, $node['delimiter']); $childrenNode[tree::CHILDREN][] = array( - tree::ID=>$nodeId, + tree::ID=> $nodeId, tree::AUTOLOAD_CHILDREN => $fn_nodeHasChildren($node), tree::CHILDREN =>array(), - tree::LABEL =>$node['MAILBOX'] + tree::LABEL => $nodeData['text'], + tree::TOOLTIP => $nodeData['tooltip'], + tree::IMAGE_LEAF => self::$leafImages['folderLeaf'], + tree::IMAGE_FOLDER_OPEN => self::$leafImages['folderOpen'], + tree::IMAGE_FOLDER_CLOSED => self::$leafImages['folderClose'] ); } $tree[tree::CHILDREN][0] = $childrenNode; } - else + else //Top Level Nodes loader { $baseNode = array('id' => 0); foreach(emailadmin_account::search(true, false) as $acc_id => $accObj) @@ -137,9 +182,9 @@ class mail_tree $baseNode = array('id' => $acc_id, 'text' => str_replace(array('<','>'),array('[',']'),$identity), 'tooltip' => '('.$acc_id.') '.htmlspecialchars_decode($identity), - 'im0' => 'thunderbird.png', - 'im1' => 'thunderbird.png', - 'im2' => 'thunderbird.png', + 'im0' => self::$leafImages['folderAccount'], + 'im1' => self::$leafImages['folderAccount'], + 'im2' => self::$leafImages['folderAccount'], 'path'=> array($acc_id), 'child'=> true, // dynamic loading on unfold 'parent' => '', @@ -166,9 +211,16 @@ class mail_tree tree::ID=>$_profileID.self::$delimiter.$foldersList[$index]['MAILBOX'], tree::AUTOLOAD_CHILDREN => $fn_nodeHasChildren($foldersList[$index]), tree::CHILDREN =>array(), - tree::LABEL =>$foldersList[$index]['MAILBOX'], - tree::OPEN => 1 + tree::LABEL =>lang($foldersList[$index]['MAILBOX']), + tree::OPEN => 1, + tree::TOOLTIP => lang($foldersList[$index]['MAILBOX']) ); + if ($index === "INBOX") + { + $parentNode[tree::IMAGE_LEAF] = self::$leafImages['folderLeaf']; + $parentNode[tree::IMAGE_FOLDER_OPEN] = self::$leafImages['folderOpen']; + $parentNode[tree::IMAGE_FOLDER_CLOSED] = self::$leafImages['folderClose']; + } // Save parentNodes $parentNodes []= $index; // Remove the parent nodes from the list @@ -180,24 +232,48 @@ class mail_tree foreach ($parentNodes as $pIndex => $parent) { $indxPattern = '/^'.$parent.'/'; - $childrenNode = array(); - + $childrenNodes = $childNode = array(); + $definedFolders = array( + 'Trash' => $this->ui->mail_bo->getTrashFolder(false), + 'Templates' => $this->ui->mail_bo->getTemplateFolder(false), + 'Drafts' => $this->ui->mail_bo->getDraftFolder(false), + 'Sent' => $this->ui->mail_bo->getSentFolder(false), + 'Junk' => $this->ui->mail_bo->getJunkFolder(false), + 'Outbox' => $this->ui->mail_bo->getOutboxFolder(false), + ); foreach ($foldersList as &$node) { - $textMatch = explode($node['delimiter'], $node['MAILBOX']); - $text = $textMatch[count($textMatch)-1]; + $pathArr = explode($node['delimiter'], $node['MAILBOX']); + $folderName = array_pop($pathArr); + $parentPath = $_profileID.self::$delimiter.implode($pathArr,$node['delimiter']); if (!preg_match($indxPattern, $node['MAILBOX'])) continue; $nodeId = $_profileID.self::$delimiter.$node['MAILBOX']; - - $childrenNode[] = array( - tree::ID=>$nodeId, + + $childNode = array( + tree::ID => $nodeId, tree::AUTOLOAD_CHILDREN => $fn_nodeHasChildren($node), - tree::CHILDREN =>array(), - tree::LABEL =>$text, + tree::CHILDREN => array(), + tree::LABEL => lang($folderName), + 'parent' => $parentPath ); + + if (array_search($node['MAILBOX'], $definedFolders) !== false) + { + //User defined folders icons + $childNode[tree::IMAGE_LEAF] = + $childNode[tree::IMAGE_FOLDER_OPEN] = + $childNode [tree::IMAGE_FOLDER_CLOSED] = "MailFolder".$folderName.".png"; + } + else + { + $childNode[tree::IMAGE_LEAF] = self::$leafImages['folderLeaf']; + $childNode[tree::IMAGE_FOLDER_OPEN] = self::$leafImages['folderOpen']; + $childNode[tree::IMAGE_FOLDER_CLOSED] = self::$leafImages['folderClose']; + } + $childrenNodes[] = $childNode; } - $baseNode[tree::CHILDREN][$pIndex][tree::CHILDREN] = $childrenNode; + $baseNode[tree::CHILDREN][$pIndex][tree::CHILDREN] = $childrenNodes; } $tree[tree::CHILDREN][0] = $baseNode; } From d9723648fd455afdf5dc0c05be32780b29bd8a46 Mon Sep 17 00:00:00 2001 From: Klaus Leithoff Date: Tue, 21 Jul 2015 15:23:18 +0000 Subject: [PATCH 006/115] adapt SendMail, and getSearchResultsMailbox --- mail/inc/class.mail_zpush.inc.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mail/inc/class.mail_zpush.inc.php b/mail/inc/class.mail_zpush.inc.php index ae73fdce87..b7af40ad5f 100644 --- a/mail/inc/class.mail_zpush.inc.php +++ b/mail/inc/class.mail_zpush.inc.php @@ -458,7 +458,7 @@ class mail_zpush implements activesync_plugin_write, activesync_plugin_sendmail, if ($this->debugLevel>2) debugLog(__METHOD__."(".__LINE__.")".' ProfileID:'.self::$profileID.' ActiveMailProfile:'.array2string($activeMailProfile)); // initialize the new egw_mailer object for sending - $mailObject = new egw_mailer(); + $mailObject = new egw_mailer(self::$profileID); $this->mail->parseRawMessageIntoMailObject($mailObject,$smartdata->mime); // Horde SMTP Class uses utf-8 by default. as we set charset always to utf-8 $mailObject->Sender = $activeMailProfile['ident_email']; @@ -1623,15 +1623,13 @@ class mail_zpush implements activesync_plugin_write, activesync_plugin_sendmail, $folderid = $searchquery['searchfolderid']; } // other types may be possible - we support quicksearch first (freeText in subject and from (or TO in Sent Folder)) - if (is_null(emailadmin_imapbase::$supportsORinQuery) || !isset(emailadmin_imapbase::$supportsORinQuery[$this->mail->profileID])) + if (is_null(emailadmin_imapbase::$supportsORinQuery) || !isset(emailadmin_imapbase::$supportsORinQuery[self::$profileID])) { emailadmin_imapbase::$supportsORinQuery = egw_cache::getCache(egw_cache::INSTANCE,'email','supportsORinQuery'.trim($GLOBALS['egw_info']['user']['account_id']),$callback=null,$callback_params=array(),$expiration=60*60*10); - if (!isset(emailadmin_imapbase::$supportsORinQuery[$this->mail->profileID])) emailadmin_imapbase::$supportsORinQuery[$this->mail->profileID]=true; + if (!isset(emailadmin_imapbase::$supportsORinQuery[self::$profileID])) emailadmin_imapbase::$supportsORinQuery[self::$profileID]=true; } - if (isset($searchquery['searchfreetext'])) { - $type = (emailadmin_imapbase::$supportsORinQuery[$this->mail->profileID]?'quick':'subject'); $searchText = $searchquery['searchfreetext']; } if (!$folderid) @@ -1641,8 +1639,9 @@ class mail_zpush implements activesync_plugin_write, activesync_plugin_sendmail, } //$_filter = array('status'=>array('UNDELETED'),'type'=>"SINCE",'string'=> date("d-M-Y", $cutoffdate)); $rv = $this->splitID($folderid,$account,$_folderName,$id); + debugLog(__METHOD__.__LINE__.' ProfileID:'.self::$profileID.' FolderID:'.$folderid.' Foldername:'.$_folderName); $this->_connect($account); - $_filter = array('type'=> (emailadmin_imapbase::$supportsORinQuery[$this->mail->profileID]?'quick':'subject'), + $_filter = array('type'=> (emailadmin_imapbase::$supportsORinQuery[self::$profileID]?'quick':'subject'), 'string'=> $searchText, 'status'=>'any', ); From 1ac7324243c9f26081784125739ca86adb559c67 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 21 Jul 2015 23:45:38 +0000 Subject: [PATCH 007/115] Calendar et2 conversion work in progress. - Drag & drop reschedule across days/weeks (grid views) --- calendar/inc/class.calendar_ui.inc.php | 1 + calendar/inc/class.calendar_uiforms.inc.php | 44 +++- calendar/js/app.js | 75 ++++-- calendar/js/et2_widget_daycol.js | 47 +++- calendar/js/et2_widget_event.js | 35 ++- calendar/js/et2_widget_timegrid.js | 266 ++++++++++---------- 6 files changed, 289 insertions(+), 179 deletions(-) diff --git a/calendar/inc/class.calendar_ui.inc.php b/calendar/inc/class.calendar_ui.inc.php index bb8b4b87bf..b751403fc3 100644 --- a/calendar/inc/class.calendar_ui.inc.php +++ b/calendar/inc/class.calendar_ui.inc.php @@ -833,6 +833,7 @@ class calendar_ui $event['app_id'] .= ':'.$event['recur_date']; } $event['parts'] = implode(",\n",$this->bo->participants($event,true)); + $event['date'] = $this->bo->date2string($event['start']); // Change dates foreach(calendar_egw_record::$types['date-time'] as $field) diff --git a/calendar/inc/class.calendar_uiforms.inc.php b/calendar/inc/class.calendar_uiforms.inc.php index 38d5689f04..7f4098e7f2 100644 --- a/calendar/inc/class.calendar_uiforms.inc.php +++ b/calendar/inc/class.calendar_uiforms.inc.php @@ -914,6 +914,11 @@ class calendar_uiforms extends calendar_ui { // Directly update stored data. If event is still visible, it will // be notified & update itself. + if(!$old_event) + { + // For new events, make sure we have the whole event, not just form data + $event = $this->bo->read($event['id']); + } $this->to_client($event); $response->call('egw.dataStoreUID','calendar::'.$event['id'],$event); } @@ -1036,7 +1041,7 @@ class calendar_uiforms extends calendar_ui } else { - egw_framework::refresh_opener($msg, 'calendar', $content['id'], $button == 'save' ? 'update' : 'delete'); + egw_framework::refresh_opener($msg, 'calendar', $event['id'], $button == 'save' ? ($content['id'] ? 'update' : 'add') : 'delete'); } egw_framework::window_close(); common::egw_exit(); @@ -2549,10 +2554,29 @@ class calendar_uiforms extends calendar_ui function ajax_moveEvent($_eventId,$calendarOwner,$targetDateTime,$targetOwner,$durationT=null) { // we do not allow dragging into another users calendar ATM - if(!$calendarOwner == $targetOwner) + if($targetOwner < 0) + { + $targetOwner = [$targetOwner]; + } + if($calendarOwner !== $targetOwner && !is_array($targetOwner)) { return false; } + // But you may be viewing multiple users, or a group calendar and + // dragging your event + if(is_array($targetOwner) && !in_array($calendarOwner, $targetOwner)) + { + $return = true; + foreach($targetOwner as $owner) + { + if($owner < 0 && in_array($calendarOwner, $GLOBALS['egw']->accounts->members($owner,true))) + { + $return = false; + break; + } + } + if($return) return; + } list($eventId, $date) = explode(':', $_eventId); $old_event=$event=$this->bo->read($eventId); if (!$durationT) @@ -2608,17 +2632,23 @@ class calendar_uiforms extends calendar_ui } } - $conflicts=$this->bo->update($event); + $message = false; + $conflicts=$this->bo->update($event,false, true, false, true, $message); $response = egw_json_response::get(); - if(!is_array($conflicts)) + if(!is_array($conflicts) && $conflicts) { // Directly update stored data. If event is still visible, it will // be notified & update itself. $this->to_client($event); $response->call('egw.dataStoreUID','calendar::'.$event['id'].($date?':'.$date:''),$event); + + if(!$sameday ) + { + $response->call('egw.refresh', '','calendar',$event['id'],'update'); + } } - else + else if ($conflicts) { $response->call( 'egw_openWindowCentered2', @@ -2630,6 +2660,10 @@ class calendar_uiforms extends calendar_ui .'&cancel_needs_refresh=true', '',750,410); } + else if ($message) + { + $response->call('egw.message', implode('
', $message)); + } if ($status_reset_to_unknown) { foreach((array)$event['participants'] as $uid => $status) diff --git a/calendar/js/app.js b/calendar/js/app.js index 682279ea6e..5feaf082ca 100644 --- a/calendar/js/app.js +++ b/calendar/js/app.js @@ -247,19 +247,19 @@ app.classes.calendar = AppJS.extend( } break; case 'calendar': - debugger; var event = egw.dataGetUIDdata('calendar::'+_id); if(event && event.data && event.data.date) { var new_cache_id = this._daywise_cache_id(event.data.date) - var daywise = egw.dataGetUIDdata(new_cache_id).data || []; + var daywise = egw.dataGetUIDdata(new_cache_id); + daywise = daywise ? daywise.data : []; if(_type === 'delete') { daywise.splice(daywise.indexOf(_id),1); } - else + else if (daywise.indexOf(_id) < 0) { - daywise.push(event.date); + daywise.push(_id); } egw.dataStoreUID(new_cache_id,daywise); } @@ -338,8 +338,9 @@ app.classes.calendar = AppJS.extend( var $sortItem = jQuery(this); }, - start: function () + start: function (event, ui) { + $j('.calendar_calTimeGrid',ui.helper).css('position', 'absolute'); // Put owners into row IDs app.classes.calendar.views[state.view].etemplates[0].widgetContainer.iterateOver(function(widget) { widget.div.parents('tr').attr('data-owner',widget.options.owner); @@ -363,7 +364,7 @@ app.classes.calendar = AppJS.extend( // Enable or disable if((state.view == 'day' || state.view == 'week') && - state.owner.length > 1 && state.owner.length > egw.config('calview_no_consolidate','phpgwapi')) + state.owner.length > 1 && state.owner.length < egw.config('calview_no_consolidate','phpgwapi')) { sortable.sortable('enable') .sortable("refresh") @@ -414,26 +415,7 @@ app.classes.calendar = AppJS.extend( var view = app.classes.calendar.views[app.calendar.state.view] || false; if (view) { - if(direction > 0) - { - start = view.end_date({date:start}); - } - else - { - start = view.start_date({date:start}); - } - start.setUTCDate(start.getUTCDate()+direction); - } - // Calculate the current difference, and move - else if(app.calendar.state.first && app.calendar.state.last) - { - start = new Date(app.calendar.state.first); - end = new Date(app.calendar.state.last); - // Get the number of days - delta = (Math.round(Math.max(1,end - start)/(24*3600*1000)))*24*3600*1000; - // Adjust - start = new Date(start.valueOf() + (delta * direction )); - end = new Date(end.valueOf() + (delta * direction)); + start = view.scroll(direction * delta); } app.calendar.update_state({date:app.calendar.date.toString(start)}); @@ -516,7 +498,7 @@ app.classes.calendar = AppJS.extend( { egw().json( 'calendar.calendar_uiforms.ajax_moveEvent', - [widget.id, widget.options.value.owner, widget.options.value.start, widget.options.value.owner, widget.options.value.duration] + [widget.options.value.id, widget.options.value.owner, widget.options.value.start, widget.options.value.owner, widget.options.value.duration] ).sendRequest(true); }, @@ -2137,9 +2119,23 @@ app.classes.calendar = AppJS.extend( d.setUTCMilliseconds(0); return d; }, + /** + * Get the owner for this view + * + * This is always the owner from the given state, we use a function + * to trigger setting the widget value. + * + * @param {number[]|String} state.owner List of owner IDs, or a comma seperated list + * @returns {number[]|String} + */ owner: function(state) { return state.owner || 0; }, + /** + * Should the view show the weekends + * + * @returns {boolean} Current preference to show 5 or 7 days in weekview + */ show_weekend: function(state) { return parseInt(egw.preference('days_in_weekview','calendar')) == 7; @@ -2147,6 +2143,19 @@ app.classes.calendar = AppJS.extend( extend: function(sub) { return jQuery.extend({},this,{_super:this},sub); + }, + /** + * Determines the new date after scrolling. The default is 1 week. + * + * @param {number} delta Integer for how many 'ticks' to move, positive for + * forward, negative for backward + * @returns {Date} + */ + scroll: function(delta) + { + var d = new Date(app.calendar.state.date); + d.setUTCDate(d.getUTCDate() + (7 * delta)); + return d; } } }); @@ -2177,6 +2186,12 @@ jQuery.extend(app.classes.calendar,{ state.days = '1'; return app.calendar.View.show_weekend.call(this,state); + }, + scroll: function(delta) + { + var d = new Date(app.calendar.state.date); + d.setUTCDate(d.getUTCDate() + (delta)); + return d; } }), day4: app.classes.calendar.prototype.View.extend({ @@ -2251,6 +2266,12 @@ jQuery.extend(app.classes.calendar,{ if(week_start < d) week_start.setUTCHours(24*7); week_start.setUTCHours(week_start.getUTCHours()-1); return week_start; + }, + scroll: function(delta) + { + var d = new Date(app.calendar.state.date); + d.setUTCMonth(d.getUTCMonth() + delta); + return d; } }), diff --git a/calendar/js/et2_widget_daycol.js b/calendar/js/et2_widget_daycol.js index 09ec11b88b..b27a238fce 100644 --- a/calendar/js/et2_widget_daycol.js +++ b/calendar/js/et2_widget_daycol.js @@ -181,9 +181,9 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea } else if(typeof _date === "string") { - this._parent.date_helper.set_year(_date.substring(0,4)); - this._parent.date_helper.set_month(_date.substring(4,6)); - this._parent.date_helper.set_date(_date.substring(6,8)); + // Need a new date to avoid invalid month/date combinations when setting + // month then day + this._parent.date_helper.set_value(new Date(_date.substring(0,4),_date.substring(4,6)-1,_date.substring(6,8))); } this.date = new Date(this._parent.date_helper.getValue()); @@ -225,7 +225,11 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea var events = []; for(var i = 0; i < event_ids.length; i++) { - events.push(egw.dataGetUIDdata('calendar::'+event_ids[i]).data); + var event = egw.dataGetUIDdata('calendar::'+event_ids[i]).data; + if(event && event.date && event.date === this.options.date) + { + events.push(event); + } } this._update_events(events); },this,this.getInstanceManager().execId,this.id); @@ -253,7 +257,11 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea var events = []; for(var i = 0; i < event_ids.length; i++) { - events.push(egw.dataGetUIDdata('calendar::'+event_ids[i]).data); + var event = egw.dataGetUIDdata('calendar::'+event_ids[i]).data; + if(event && event.date && event.date === this.options.date) + { + events.push(event); + } } this._update_events(events); },this,this.getInstanceManager().execId,this.id); @@ -322,16 +330,24 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea */ _update_events: function(_events) { - // Remove all events - while(this._children.length) + var events = _events || this.getArrayMgr('content').getEntry(this.options.date) || []; + + // Remove extra events + while(this._children.length > events.length) { var node = this._children[this._children.length-1]; this.removeChild(node); node.free(); } - var events = _events || this.getArrayMgr('content').getEntry(this.options.date) || []; - - for(var c = 0; c < events.length; c++) + + // Make sure children are in cronological order, or columns are backwards + events.sort(function(a,b) { + var start = new Date(a.start) - new Date(b.start); + var end = new Date(a.end) - new Date(b.end); + return a.whole_day ? -1 : (start ? start : end); + }); + + for(var c = this._children.length; c < events.length; c++) { // Create event var event = et2_createWidget('calendar-event',{ @@ -348,7 +364,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea } // Seperate loop so column sorting finds all children in the right place - for(var c = 0; c < events.length; c++) + for(var c = 0; c < events.length && c < this._children.length; c++) { this._children[c].set_value(events[c]); } @@ -386,7 +402,14 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizea { var event = this._children[i].options.value || false; if(!event) continue; - + if(event.date && event.date != this.options.date) + { + // Still have a child event that has changed date (DnD) + this._children[i].destroy(); + this.removeChild(this._children[i]); + continue; + } + var c = 0; event['multiday'] = false; if(typeof event.start !== 'object') diff --git a/calendar/js/et2_widget_event.js b/calendar/js/et2_widget_event.js index 952ec2069e..534c03e6af 100644 --- a/calendar/js/et2_widget_event.js +++ b/calendar/js/et2_widget_event.js @@ -99,13 +99,33 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], // Register for updates var app_id = this.options.value.app_id ? this.options.value.app_id : this.options.value.id + (this.options.value.recur_type ? ':'+this.options.value.recur_date : ''); egw.dataRegisterUID('calendar::'+app_id, function(event) { + if(this._parent && this.options.value.date && event.date != this.options.value.date) + { + // Date changed, reparent + var new_parent = this._parent._parent.getWidgetById(event.date); + if(new_parent) + { + new_parent.addChild(this); + } + else + { + // Could not find the right date + this._parent.removeChild(this); + this.destroy(); + } + return; + } // Copy to avoid changes, which may cause nm problems this.options.value = jQuery.extend({},event); // Let parent position this._parent.position_event(this); - - this._update(this.options.value); + + // Parent may remove this if the date isn't the same + if(this._parent) + { + this._update(this.options.value); + } },this,this.getInstanceManager().execId,this.id); @@ -123,10 +143,10 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], var eventId = event.id.match(/-?\d+\.?\d*/g)[0]; var appName = event.id.replace(/-?\d+\.?\d*/g,''); var app_id = event.app_id ? event.app_id : event.id + (event.recur_type ? ':'+event.recur_date : ''); - this._parent.date_helper.set_value(event.start); + this._parent.date_helper.set_value(event.start.valueOf ? new Date(event.start) : event.start); var formatted_start = this._parent.date_helper.getValue(); - this.set_id(eventId || event.id); + this.set_id('event_' + (eventId || event.id)); this.div // Empty & re-append to make sure dnd helpers are gone @@ -156,7 +176,8 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], // Remove any resize classes, the handles are gone due to empty() .removeClass('ui-resizable') .addClass(event.class) - .toggleClass('calendar_calEventPrivate', event.private) + .toggleClass('calendar_calEventPrivate', event.private); + this.options.class = event.class; if(event.category) { this.div.addClass('cat_' + event.category); @@ -240,9 +261,9 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM], var bg_color = this.div.css('background-color'); var header_color = this.title.css('color'); - this._parent.date_helper.set_value(this.options.value.start); + this._parent.date_helper.set_value(this.options.value.start.valueOf ? new Date(this.options.value.start) : this.options.value.start); var start = this._parent.date_helper.input_date.val(); - this._parent.date_helper.set_value(this.options.value.end); + this._parent.date_helper.set_value(this.options.value.end.valueOf ? new Date(this.options.value.end) : this.options.value.end); var end = this._parent.date_helper.input_date.val(); var times = !this.options.value.multiday ? diff --git a/calendar/js/et2_widget_timegrid.js b/calendar/js/et2_widget_timegrid.js index 20b0ba0e02..c47a4eb5ec 100644 --- a/calendar/js/et2_widget_timegrid.js +++ b/calendar/js/et2_widget_timegrid.js @@ -160,35 +160,6 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz // - no action system - var timegrid = this; - // Show the current time while dragging - // Used for resizing as well as drag & drop - var drag_helper = function(event, element,height) - { - this.dropEnd = timegrid._get_time_from_position(element.getBoundingClientRect().left, - element.getBoundingClientRect().top+parseInt(height)); - - if (typeof this.dropEnd != 'undefined' && this.dropEnd.length) - { - this.dropEnd.addClass("drop-hover"); - var time = jQuery.datepicker.formatTime( - egw.preference("timeformat") == 12 ? "h:mmtt" : "HH:mm", - { - hour: this.dropEnd.attr('data-hour'), - minute: this.dropEnd.attr('data-minute'), - seconds: 0, - timezone: 0 - }, - {"ampm": (egw.preference("timeformat") == "12")} - ); - this.innerHTML = '
'+time+'
'; - } - else - { - this.innerHTML = '
'; - } - return this.dropEnd; - }; - /** * If user puts the mouse over an event, then we'll set up resizing so * they can adjust the length. Should be a little better on resources @@ -236,7 +207,7 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz e.originalEvent = event; e.data = {duration: 0}; var event_data = timegrid._get_event_info(this); - var event_widget = timegrid.getWidgetById(event_data.id); + var event_widget = timegrid.getWidgetById('event_'+event_data.id); var sT = event_widget.options.value.start_m; if (typeof this.dropEnd != 'undefined' && this.dropEnd.length == 1) { @@ -269,7 +240,7 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz resize:function(event, ui) { // Add 5px to make sure it doesn't land right on the edge of a div - drag_helper.call(this,event,ui.element[0],ui.helper.outerHeight()+5); + timegrid._drag_helper(this,ui.element[0],ui.helper.outerHeight()+5); } }); }); @@ -278,70 +249,106 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz this.div.on('dragcreate','.calendar_calEvent:not(.rowNoEdit)', function(event,ui) { $j(this).draggable('option','cursorAt',false); }) - .on('dragstart', '.calendar_calEvent:not(.rowNoEdit)', function(event,ui) { + .on('dragstart', '.calendar_calEvent', function(event,ui) { $j('.calendar_calEvent',ui.helper).width($j(this).width()) .height($j(this).outerHeight()) + .css('top', '').css('left','') .appendTo(ui.helper); - }) - .on('dragstop','.calendar_calEvent:not(.rowNoEdit)', function(event,ui) { - var e = new jQuery.Event('change'); - e.originalEvent = event; - e.data = {start: 0}; - if (typeof this.dropEnd != 'undefined' && this.dropEnd.length >= 1) - { - var drop_date = this.dropEnd.attr('data-date')||false; - - var eT = parseInt(this.dropEnd.attr('data-hour') * 60) + parseInt(this.dropEnd.attr('data-minute')); - - var event_data = timegrid._get_event_info(this); - var event_widget = timegrid.getWidgetById(event_data.id); - - if(event_widget) - { - event_widget._parent.date_helper.set_year(drop_date.substring(0,4)); - event_widget._parent.date_helper.set_month(drop_date.substring(4,6)); - event_widget._parent.date_helper.set_date(drop_date.substring(6,8)); - event_widget._parent.date_helper.set_hours(this.dropEnd.attr('data-hour')); - event_widget._parent.date_helper.set_minutes(this.dropEnd.attr('data-minute')); - event_widget.options.value.start = event_widget._parent.date_helper.getValue(); - - // Leave the helper there until the update is done - var loading = ui.helper.clone().appendTo(ui.helper.parent()); - loading.addClass('loading'); - - event_widget.recur_prompt(function(button_id) { - //Get infologID if in case if it's an integrated infolog event - if (event_data.app === 'infolog') - { - // If it is an integrated infolog event we need to edit infolog entry - egw().json('stylite_infolog_calendar_integration::ajax_moveInfologEvent', - [event_data.id, event_widget.options.value.start||false], - function() {loading.remove();} - ).sendRequest(true); - } - else - { - //Edit calendar event - egw().json('calendar.calendar_uiforms.ajax_moveEvent', [ - button_id=='series' ? event_data.id : event_data.app_id,event_data.owner, - event_widget.options.value.start, - timegrid.options.owner||egw.user('account_id') - ], - function() { loading.remove();} - ).sendRequest(true); - } - }); - } - } - }) - // As event is dragged, update the time - .on('drag', '.calendar_calEvent:not(.rowNoEdit)', function(event,ui) { - this.dropEnd = drag_helper.call($j('.calendar_calEventHeader',ui.helper)[0],event,ui.helper[0],0); - $j('.calendar_timeDemo',ui.helper).css('bottom','auto'); + ui.helper.width($j(this).width()); }); return true; }, + /** + * Show the current time while dragging + * Used for resizing as well as drag & drop + */ + _drag_helper: function(element, helper,height) + { + element.dropEnd = this._get_time_from_position(helper.getBoundingClientRect().left, + helper.getBoundingClientRect().top+parseInt(height)); + + if (typeof element.dropEnd != 'undefined' && element.dropEnd.length) + { + element.dropEnd.addClass("drop-hover"); + var time = jQuery.datepicker.formatTime( + egw.preference("timeformat") == 12 ? "h:mmtt" : "HH:mm", + { + hour: element.dropEnd.attr('data-hour'), + minute: element.dropEnd.attr('data-minute'), + seconds: 0, + timezone: 0 + }, + {"ampm": (egw.preference("timeformat") == "12")} + ); + element.innerHTML = '
'+time+'
'; + } + else + { + element.innerHTML = '
'; + } + $j(element).width($j(helper).width()); + return element.dropEnd; + }, + + /** + * Handler for dropping an event on the timegrid + */ + _event_drop: function(timegrid, event,ui) { + var e = new jQuery.Event('change'); + e.originalEvent = event; + e.data = {start: 0}; + if (typeof this.dropEnd != 'undefined' && this.dropEnd.length >= 1) + { + var drop_date = this.dropEnd.attr('data-date')||false; + + var event_data = timegrid._get_event_info(ui.draggable); + var event_widget = timegrid.getWidgetById('event_'+event_data.id); + if(!event_widget) + { + // Widget was moved across weeks / owners + event_widget = timegrid.getParent().getWidgetById('event_'+event_data.id); + } + if(event_widget) + { + event_widget._parent.date_helper.set_year(drop_date.substring(0,4)); + event_widget._parent.date_helper.set_month(drop_date.substring(4,6)); + event_widget._parent.date_helper.set_date(drop_date.substring(6,8)); + event_widget._parent.date_helper.set_hours(this.dropEnd.attr('data-hour')); + event_widget._parent.date_helper.set_minutes(this.dropEnd.attr('data-minute')); + event_widget.options.value.start = new Date(event_widget._parent.date_helper.getValue()); + + // Leave the helper there until the update is done + var loading = ui.helper.clone().appendTo(ui.helper.parent()); + loading.addClass('loading'); + + event_widget.recur_prompt(function(button_id) { + if(button_id === 'cancel' || !button_id) return; + //Get infologID if in case if it's an integrated infolog event + if (event_data.app === 'infolog') + { + // If it is an integrated infolog event we need to edit infolog entry + egw().json('stylite_infolog_calendar_integration::ajax_moveInfologEvent', + [event_data.id, event_widget.options.value.start||false], + function() {loading.remove();} + ).sendRequest(true); + } + else + { + //Edit calendar event + egw().json('calendar.calendar_uiforms.ajax_moveEvent', [ + button_id==='series' ? event_data.id : event_data.app_id,event_data.owner, + event_widget.options.value.start, + timegrid.options.owner||egw.user('account_id') + ], + function() { loading.remove();} + ).sendRequest(true); + } + }); + } + } + }, + /** * Something changed, and the days need to be re-drawn. We wait a bit to * avoid re-drawing twice if start and end date both changed, then recreate @@ -381,7 +388,7 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz detachFromDOM: function() { // Remove the binding to the change handler - $j(this.div).off("change.et2_calendar_timegrid"); + $j(this.div).off(".et2_calendar_timegrid"); this._super.apply(this, arguments); }, @@ -636,42 +643,55 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz // Determine target node var event = _data.event || false; if(!event) return; - + if(_data.ui.draggable.hasClass('rowNoEdit')) return; + + /* + We have to handle the drop in the normal event stream instead of waiting + for the egwAction system so we can get the helper, and destination + */ + if(event.type === 'drop') + { + this.getWidget()._event_drop.call($j('.calendar_d-n-d_timeCounter',_data.ui.helper)[0],this.getWidget(),event, _data.ui); + } + var drag_listener = function(event, ui) { + aoi.getWidget()._drag_helper($j('.calendar_d-n-d_timeCounter',ui.helper)[0],ui.helper[0],0); + }; + var time = $j('.calendar_d-n-d_timeCounter',_data.ui.helper); switch(_event) { // Triggered once, when something is dragged into the timegrid's div case EGW_AI_DRAG_OVER: - _data.ui.draggable.off('.et2_timegrid') - // Listen to the drag and update the helper with the time - .on('drag.et2_timegrid',function(event,ui) { - var nodes = aoi.getWidget()._get_time_from_position(event.clientX,event.clientY); + // Listen to the drag and update the helper with the time + // This part lets us drag between different timegrids + _data.ui.draggable.on('drag.et2_timegrid'+widget_object.id, drag_listener); + _data.ui.draggable.on('dragend.et2_timegrid'+widget_object.id, function() { + _data.ui.draggable.off('drag.et2_timegrid' + widget_object.id); + }); + if(time.length) + { + // The out will trigger after the over, so we count + time.data('count',time.data('count')+1); + } + else + { + _data.ui.helper.prepend('
'); + } - // Highlight the destination time - $j('[data-date]',aoi.doGetDOMNode()).removeClass("ui-state-active"); - nodes.addClass('ui-state-active'); - - // Update the helper with the actual time - var time = jQuery.datepicker.formatTime( - egw.preference("timeformat") == 12 ? "h:mmtt" : "HH:mm", - { - hour: nodes.attr('data-hour'), - minute: nodes.attr('data-minute'), - seconds: 0, - timezone: 0 - }, - {"ampm": (egw.preference("timeformat") == "12")} - ); - $j('.calendar_d-n-d_timeCounter span',ui.helper).empty().html(time); - }) - _data.ui.helper.prepend('
'); break; // Triggered once, when something is dragged out of the timegrid case EGW_AI_DRAG_OUT: - // Reset + // Stop listening + _data.ui.draggable.off('drag.et2_timegrid'+widget_object.id); + // Remove any highlighted time squares $j('[data-date]',this.doGetDOMNode()).removeClass("ui-state-active"); - _data.ui.draggable.off('.et2_timegrid'); - $j('.calendar_d-n-d_timeCounter',_data.ui.helper[0]).remove(); + + // Out triggers after the over, count to not accidentally remove + time.data('count',time.data('count')-1); + if(time.length && time.data('count') <= 0) + { + time.remove(); + } break; } }; @@ -804,17 +824,7 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz { // Create drag action that allows linking drag_action = mgr.addAction('drag', 'egw_link_drag', egw.lang('link'), 'link', function(action, selected) { - // Drag helper - list titles. Arbitrarily limited to 10. - var helper = $j(document.createElement("div")); - for(var i = 0; i < selected.length && i < 10; i++) - { - var id = selected[i].id.split('::'); - var span = $j(document.createElement('span')).appendTo(helper); - egw.link_title(id[0],id[1], function(title) { - this.append(title); - this.append('
'); - }, span); - } + // Drag helper - list titles. // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links // TODO: Need to decide if we need to create a customized helper interface for links anyway //return helper; @@ -823,7 +833,7 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz } if(actionLinks.indexOf(drag_action.id) < 0) { - //actionLinks.push(drag_action.id); + actionLinks.push(drag_action.id); } drag_action.set_dragType('link'); }, @@ -1038,7 +1048,7 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz if (this.onevent_change) { var event_data = this._get_event_info(dom_node); - var event_widget = this.getWidgetById(event_data.id); + var event_widget = this.getWidgetById('event_'+event_data.id); et2_calendar_event.recur_prompt(event_data, jQuery.proxy(function(button_id, event_data) { // No need to continue if(button_id === 'cancel') return false; From ae4fa08e80c37534ab60d501db1271fb26797c78 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Wed, 22 Jul 2015 10:04:24 +0000 Subject: [PATCH 008/115] Add checked option to tree widget --- etemplate/inc/class.etemplate_widget_tree.inc.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/etemplate/inc/class.etemplate_widget_tree.inc.php b/etemplate/inc/class.etemplate_widget_tree.inc.php index 62153b91ea..ce7fbe78fa 100644 --- a/etemplate/inc/class.etemplate_widget_tree.inc.php +++ b/etemplate/inc/class.etemplate_widget_tree.inc.php @@ -32,6 +32,7 @@ egw_framework::includeCSS('/phpgwapi/js/dhtmlxtree/codebase/dhtmlXTree.css'); * array(tree::ID => '/INBOX/sub', tree::LABEL => 'sub', tree::IMAGE_LEAF => 'folderClosed.gif'), * array(tree::ID => '/INBOX/sub2', tree::LABEL => 'sub2', tree::IMAGE_LEAF => 'folderClosed.gif'), * ), + * tree::CHECKED => true * ), * array( * tree::ID => '/user', @@ -39,7 +40,8 @@ egw_framework::includeCSS('/phpgwapi/js/dhtmlxtree/codebase/dhtmlXTree.css'); * tree::CHILDREN => array( * array(tree::ID => '/user/birgit', tree::LABEL => 'birgit', tree::IMAGE_LEAF => 'folderClosed.gif'), * array(tree::ID => '/user/ralf', tree::LABEL => 'ralf', tree::AUTOLOAD_CHILDREN => 1), - * ) + * ), + * tree::CHECKED => false * ), * )); * @@ -95,7 +97,12 @@ class etemplate_widget_tree extends etemplate_widget * key of flag if folder is open, default folder is closed */ const OPEN = 'open'; - + + /** + * check checkbox if exists (in case of three-state checkboxes values can be:0 unchecked- 1 - checked or -1 - unsure) + */ + const CHECKED = 0; + /** * Parse and set extra attributes from xml in template object * From 466eea3cbcccefb9fb3b2fe439a649b2d4ab11c9 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Wed, 22 Jul 2015 10:06:30 +0000 Subject: [PATCH 009/115] Correct the value of checked option from previous commit --- etemplate/inc/class.etemplate_widget_tree.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etemplate/inc/class.etemplate_widget_tree.inc.php b/etemplate/inc/class.etemplate_widget_tree.inc.php index ce7fbe78fa..0629f98d80 100644 --- a/etemplate/inc/class.etemplate_widget_tree.inc.php +++ b/etemplate/inc/class.etemplate_widget_tree.inc.php @@ -101,7 +101,7 @@ class etemplate_widget_tree extends etemplate_widget /** * check checkbox if exists (in case of three-state checkboxes values can be:0 unchecked- 1 - checked or -1 - unsure) */ - const CHECKED = 0; + const CHECKED = 'checked'; /** * Parse and set extra attributes from xml in template object From fbc6eb969b371232553eb04bbf155a378927b58a Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Wed, 22 Jul 2015 11:58:41 +0000 Subject: [PATCH 010/115] Do not hide selectbox if the hover is on selectbox options -Fix quick_add selectbox flickering in FF39 --- pixelegg/js/slider.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pixelegg/js/slider.js b/pixelegg/js/slider.js index 00e974ee07..9cf3a0a917 100644 --- a/pixelegg/js/slider.js +++ b/pixelegg/js/slider.js @@ -160,7 +160,10 @@ egw_LAB.wait(function() { }, mouseout: function(ev){ // do NOT react on bubbeling events from contained selectbox - if (ev.target && ev.target.id != 'quick_add_selectbox' && ev.relatedTarget.id != 'quick_add' && ev.relatedTarget.id !='quick_add_selectbox') + if (ev.target && ev.relatedTarget && ev.target.id != 'quick_add_selectbox' + && ev.relatedTarget.id != 'quick_add' + && ev.relatedTarget.id !='quick_add_selectbox' + && ev.relatedTarget.tagName != "OPTION") { $j(this).css({ transition: "0.6s ease-out 0s", From 28057d583a9c8f39634ec57e4e17ed0ad8cfd271 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Wed, 22 Jul 2015 12:43:10 +0000 Subject: [PATCH 011/115] W.I.P. mail tree -Add Checked option to mail tree structure --- mail/inc/class.mail_tree.inc.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mail/inc/class.mail_tree.inc.php b/mail/inc/class.mail_tree.inc.php index cfbeef506c..c50b84e580 100644 --- a/mail/inc/class.mail_tree.inc.php +++ b/mail/inc/class.mail_tree.inc.php @@ -132,7 +132,7 @@ class mail_tree return $hasChildren; }; - if ($_parent) $_profileID = $this->ui->mail_bo->icServerID; + if ($_parent) $_profileID = $this->ui->mail_bo->profileID; if (is_numeric($_profileID) && $_profileID != $this->ui->mail_bo->profileID) { @@ -167,7 +167,8 @@ class mail_tree tree::TOOLTIP => $nodeData['tooltip'], tree::IMAGE_LEAF => self::$leafImages['folderLeaf'], tree::IMAGE_FOLDER_OPEN => self::$leafImages['folderOpen'], - tree::IMAGE_FOLDER_CLOSED => self::$leafImages['folderClose'] + tree::IMAGE_FOLDER_CLOSED => self::$leafImages['folderClose'], + tree::CHECKED => $node['SUBSCRIBED'] ); } $tree[tree::CHILDREN][0] = $childrenNode; @@ -213,7 +214,8 @@ class mail_tree tree::CHILDREN =>array(), tree::LABEL =>lang($foldersList[$index]['MAILBOX']), tree::OPEN => 1, - tree::TOOLTIP => lang($foldersList[$index]['MAILBOX']) + tree::TOOLTIP => lang($foldersList[$index]['MAILBOX']), + tree::CHECKED => $node['SUBSCRIBED'] ); if ($index === "INBOX") { @@ -255,7 +257,8 @@ class mail_tree tree::AUTOLOAD_CHILDREN => $fn_nodeHasChildren($node), tree::CHILDREN => array(), tree::LABEL => lang($folderName), - 'parent' => $parentPath + 'parent' => $parentPath, + tree::CHECKED => $node['SUBSCRIBED'] ); if (array_search($node['MAILBOX'], $definedFolders) !== false) From 262d8d6440bb1d22ffc61399e1ff102a3e26df7e Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Wed, 22 Jul 2015 13:35:01 +0000 Subject: [PATCH 012/115] set network timeout for ldap connections to not block for minutes --- phpgwapi/inc/class.ldap.inc.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpgwapi/inc/class.ldap.inc.php b/phpgwapi/inc/class.ldap.inc.php index b08d096abf..11cf3b11c3 100644 --- a/phpgwapi/inc/class.ldap.inc.php +++ b/phpgwapi/inc/class.ldap.inc.php @@ -192,6 +192,8 @@ class ldap { return False; } + // set network timeout to not block for minutes + ldap_set_option($this->ds, LDAP_OPT_NETWORK_TIMEOUT, 5); if(ldap_set_option($this->ds, LDAP_OPT_PROTOCOL_VERSION, 3)) { From d5cceaf7d6b35455b7b307406fc9ed346415190b Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Wed, 22 Jul 2015 14:22:28 +0000 Subject: [PATCH 013/115] Fix merged dates from the event list were not timestamps, and could not be formatted --- calendar/inc/class.calendar_merge.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calendar/inc/class.calendar_merge.inc.php b/calendar/inc/class.calendar_merge.inc.php index c87b00b1b0..26f84df664 100644 --- a/calendar/inc/class.calendar_merge.inc.php +++ b/calendar/inc/class.calendar_merge.inc.php @@ -220,7 +220,7 @@ class calendar_merge extends bo_merge 'time' => (date('Ymd',$event['start']) != date('Ymd',$event['end']) ? $GLOBALS['egw_info']['user']['preferences']['common']['dateformat'].' ' : '') . ($GLOBALS['egw_info']['user']['preferences']['common']['timeformat'] == 12 ? 'h:i a' : 'H:i'), ) as $name => $format) { - $value = date($format,$event[$what]); + $value = egw_time::to($event[$what],$format); if ($format == 'l') $value = lang($value); $replacements['$$' .($prefix ? $prefix.'/':'').'calendar_'.$what.$name.'$$'] = $value; } From 5bac30f8c74ebc4855049fa4a9e3fed79cb2bc0b Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Wed, 22 Jul 2015 14:44:19 +0000 Subject: [PATCH 014/115] Hide whole day label when read-only and the event is not whole day --- calendar/inc/class.calendar_uiforms.inc.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/calendar/inc/class.calendar_uiforms.inc.php b/calendar/inc/class.calendar_uiforms.inc.php index 7f4098e7f2..6bd0cade8f 100644 --- a/calendar/inc/class.calendar_uiforms.inc.php +++ b/calendar/inc/class.calendar_uiforms.inc.php @@ -1585,6 +1585,11 @@ class calendar_uiforms extends calendar_ui $content['participants']['no_add'] = true; + if(!$event['whole_day']) + { + $etpl->setElementAttribute('whole_day', 'disabled', true); + } + // respect category permissions if(!empty($event['category'])) { From 7c939f7d58e4b27c582975bd633d36211df958f6 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Wed, 22 Jul 2015 15:37:05 +0000 Subject: [PATCH 015/115] When copying a project, if info_from is the same as the old project name, change it to the new project name --- infolog/inc/class.infolog_datasource.inc.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infolog/inc/class.infolog_datasource.inc.php b/infolog/inc/class.infolog_datasource.inc.php index 5dc47fc99f..21a3308a7c 100644 --- a/infolog/inc/class.infolog_datasource.inc.php +++ b/infolog/inc/class.infolog_datasource.inc.php @@ -116,8 +116,9 @@ class infolog_datasource extends datasource if(!($info['info_id'] = $this->infolog_bo->write($info))) return false; // link the new infolog against the project and setting info_link_id and evtl. info_from + $old_link = $info['info_link_id'] ? egw_link::get_link($info['info_link']) : $info['info_link']; $info['info_link_id'] = egw_link::link('projectmanager',$target,'infolog',$info['info_id'],$element['pe_remark'],0,0,1); - if (!$info['info_from']) + if (!$info['info_from'] || $old_link && $info['info_from'] == $old_link['title']) { $info['info_from'] = egw_link::title('projectmanager',$target); } From 95123cd89cc1cc0bf9e1042b993a972b4f998f8f Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Wed, 22 Jul 2015 16:02:44 +0000 Subject: [PATCH 016/115] W.I.P mail tree -Fix wrong children items from the autoloading --- mail/inc/class.mail_tree.inc.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mail/inc/class.mail_tree.inc.php b/mail/inc/class.mail_tree.inc.php index c50b84e580..256f97eeee 100644 --- a/mail/inc/class.mail_tree.inc.php +++ b/mail/inc/class.mail_tree.inc.php @@ -159,7 +159,7 @@ class mail_tree { $nodeId = $_profileID.self::$delimiter.$node['MAILBOX']; $nodeData = self::getFolderData($nodeId, $node['delimiter']); - $childrenNode[tree::CHILDREN][] = array( + $childrenNode[] = array( tree::ID=> $nodeId, tree::AUTOLOAD_CHILDREN => $fn_nodeHasChildren($node), tree::CHILDREN =>array(), @@ -168,10 +168,11 @@ class mail_tree tree::IMAGE_LEAF => self::$leafImages['folderLeaf'], tree::IMAGE_FOLDER_OPEN => self::$leafImages['folderOpen'], tree::IMAGE_FOLDER_CLOSED => self::$leafImages['folderClose'], - tree::CHECKED => $node['SUBSCRIBED'] + tree::CHECKED => $node['SUBSCRIBED'], + 'parent' => $_parent ); } - $tree[tree::CHILDREN][0] = $childrenNode; + $tree[tree::CHILDREN] = $childrenNode; } else //Top Level Nodes loader { From e3fb882ebaf54845201f0cdf37de93161e0c5a62 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Thu, 23 Jul 2015 10:33:01 +0000 Subject: [PATCH 017/115] Do not force tree node to state of open if we are using autoloading --- etemplate/js/et2_widget_tree.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/etemplate/js/et2_widget_tree.js b/etemplate/js/et2_widget_tree.js index f4868eed2d..abd4238370 100644 --- a/etemplate/js/et2_widget_tree.js +++ b/etemplate/js/et2_widget_tree.js @@ -374,7 +374,10 @@ var et2_tree = et2_inputWidget.extend( for(var i = 0; i < this.value.length; i++) { this.input.setCheck(this.value[i], true); - this.input.openItem(this.value[i]); + // autoloading openning needs to be absolutely based on user interaction + // or open flag in folder structure, therefore, We should + // not force it to open the node + if (!this.options.autoloading) this.input.openItem(this.value[i]); } } else From 23d0108df08abf18019d527bd55ecc1ef71da747 Mon Sep 17 00:00:00 2001 From: Klaus Leithoff Date: Thu, 23 Jul 2015 11:07:46 +0000 Subject: [PATCH 018/115] prevent some javascript-errors that prevented the correct execution of code --- phpgwapi/js/jsapi/app_base.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/phpgwapi/js/jsapi/app_base.js b/phpgwapi/js/jsapi/app_base.js index 40d53785a9..e6f26bad4f 100644 --- a/phpgwapi/js/jsapi/app_base.js +++ b/phpgwapi/js/jsapi/app_base.js @@ -776,11 +776,12 @@ var AppJS = Class.extend( var match_count = 0; for(var state_key in state) { - if(state[state_key] == favorite.state[state_key] || !state[state_key] && !favorite.state[state_key]) + if(typeof favorite.state != 'undefined' && typeof state[state_key] != 'undefined'&&typeof favorite.state[state_key] != 'undefined' && ( state[state_key] == favorite.state[state_key] || !state[state_key] && !favorite.state[state_key])) { match_count++; } - else if (state[state_key] && typeof state[state_key] === 'object' && favorite.state[state_key] && typeof favorite.state[state_key] === 'object') + else if (typeof state[state_key] != 'undefined' && state[state_key] && typeof state[state_key] === 'object' + && typeof favorite.state != 'undefined' && typeof favorite.state[state_key] != 'undefined' && favorite.state[state_key] && typeof favorite.state[state_key] === 'object') { if((typeof state[state_key].length !== 'undefined' || typeof state[state_key].length !== 'undefined') && (state[state_key].length || Object.keys(state[state_key]).length) != (favorite.state[state_key].length || Object.keys(favorite.state[state_key]).length )) @@ -823,7 +824,9 @@ var AppJS = Class.extend( { // Skip, might be set, might not } - else if (typeof state[state_key] !== 'undefined' && state[state_key] != favorite.state[state_key]) + else if (typeof state[state_key] !== 'undefined' + && typeof favorite.state != 'undefined'&&typeof favorite.state[state_key] !== 'undefined' + && state[state_key] != favorite.state[state_key]) { // Different values, do not match return; From d59c15582fb0d6f066bc7743e59fc989cbd93f99 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 23 Jul 2015 11:30:57 +0000 Subject: [PATCH 019/115] fix sql error if link-id (accidently) contains non-ascii chars --- phpgwapi/inc/class.solink.inc.php | 108 ++++++++++++++++-------------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/phpgwapi/inc/class.solink.inc.php b/phpgwapi/inc/class.solink.inc.php index 19a4b4b469..3f0f5545b8 100644 --- a/phpgwapi/inc/class.solink.inc.php +++ b/phpgwapi/inc/class.solink.inc.php @@ -8,7 +8,7 @@ * * @link http://www.egroupware.org * @author Ralf Becker - * @copyright 2001-2014 by RalfBecker@outdoor-training.de + * @copyright 2001-2015 by RalfBecker@outdoor-training.de * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api * @subpackage link @@ -50,9 +50,9 @@ class solink * @param string $id1 id in $app1 * @param string $app2 appname of 2. endpoint of the link * @param string $id2 id in $app2 - * @param string $remark='' Remark to be saved with the link (defaults to '') - * @param int $owner=0 Owner of the link (defaults to user) - * @param int $lastmod=0 timestamp of last modification (defaults to now=time()) + * @param string $remark ='' Remark to be saved with the link (defaults to '') + * @param int $owner =0 Owner of the link (defaults to user) + * @param int $lastmod =0 timestamp of last modification (defaults to now=time()) * @return int/boolean False (for db or param-error) or on success link_id (Please not the return-value of $id1) */ static function link( $app1,&$id1,$app2,$id2='',$remark='',$owner=0,$lastmod=0 ) @@ -111,10 +111,10 @@ class solink * * @param string $app appname * @param string|array $id id(s) in $app - * @param string $only_app='' if set return only links from $only_app (eg. only addressbook-entries) or NOT from if $only_app[0]=='!' - * @param string $order='link_lastmod DESC' defaults to newest links first - * @param boolean $deleted=false Include links that have been flagged as deleted, waiting for purge of linked record. - * @param int|array $limit=null number of entries to return, default null = all or array(offset, num_rows) to return num_rows starting from offset + * @param string $only_app ='' if set return only links from $only_app (eg. only addressbook-entries) or NOT from if $only_app[0]=='!' + * @param string $order ='link_lastmod DESC' defaults to newest links first + * @param boolean $deleted =false Include links that have been flagged as deleted, waiting for purge of linked record. + * @param int|array $limit =null number of entries to return, default null = all or array(offset, num_rows) to return num_rows starting from offset * @return array id => links pairs if $id is an array or just the links (only_app: ids) or empty array if no matching links found */ static function get_links($app, $id, $only_app='', $order='link_lastmod DESC', $deleted=false, $limit=null) @@ -139,30 +139,36 @@ class solink } $links = array(); - foreach(self::$db->select(self::TABLE, '*', self::$db->expression(self::TABLE, '((', array( - 'link_app1' => $app, - 'link_id1' => $id, - ),') OR (',array( - 'link_app2' => $app, - 'link_id2' => $id, - ),'))', - $deleted ? '' : ' AND deleted IS NULL' - ), __LINE__, __FILE__, $offset, $order ? " ORDER BY $order" : '', 'phpgwapi', $limit) as $row) - { - // check if left side (1) is one of our targets --> add it - if ($row['link_app1'] == $app && in_array($row['link_id1'],(array)$id)) + try { + foreach(self::$db->select(self::TABLE, '*', self::$db->expression(self::TABLE, '((', array( + 'link_app1' => $app, + 'link_id1' => $id, + ),') OR (',array( + 'link_app2' => $app, + 'link_id2' => $id, + ),'))', + $deleted ? '' : ' AND deleted IS NULL' + ), __LINE__, __FILE__, $offset, $order ? " ORDER BY $order" : '', 'phpgwapi', $limit) as $row) { - self::_add2links($row,true,$only_app,$not_only,$links); - } - // check if right side (2) is one of our targets --> add it (both can be true for multiple targets!) - if ($row['link_app2'] == $app && in_array($row['link_id2'],(array)$id)) - { - self::_add2links($row,false,$only_app,$not_only,$links); + // check if left side (1) is one of our targets --> add it + if ($row['link_app1'] == $app && in_array($row['link_id1'],(array)$id)) + { + self::_add2links($row,true,$only_app,$not_only,$links); + } + // check if right side (2) is one of our targets --> add it (both can be true for multiple targets!) + if ($row['link_app2'] == $app && in_array($row['link_id2'],(array)$id)) + { + self::_add2links($row,false,$only_app,$not_only,$links); + } } + // if query returns exactly limit rows, we assume there are more and therefore set self::$limit_exceeded + self::$limit_exceeded = $offset !== false && count(is_array($id) ? $links : $links[$id]) == $limit; + } + // catch Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' (1267) + // caused by non-ascii chars compared with ascii field uid + catch(egw_exception_db $e) { + _egw_log_exception($e); } - // if query returns exactly limit rows, we assume there are more and therefore set self::$limit_exceeded - self::$limit_exceeded = $offset !== false && count(is_array($id) ? $links : $links[$id]) == $limit; - return is_array($id) ? $links : ($links[$id] ? $links[$id] : array()); } @@ -193,9 +199,9 @@ class solink * returns data of a link * * @param ing/string $app_link_id > 0 link_id of link or app-name of link - * @param string $id='' id in $app, if no integer link_id given in $app_link_id - * @param string $app2='' appname of 2. endpoint of the link, if no integer link_id given in $app_link_id - * @param string $id2='' id in $app2, if no integer link_id given in $app_link_id + * @param string $id ='' id in $app, if no integer link_id given in $app_link_id + * @param string $app2 ='' appname of 2. endpoint of the link, if no integer link_id given in $app_link_id + * @param string $id2 ='' id in $app2, if no integer link_id given in $app_link_id * @return array with link-data or False */ static function get_link($app_link_id,$id='',$app2='',$id2='') @@ -226,26 +232,26 @@ class solink 'link_id1' => $id2, ),')'); } - foreach(self::$db->select(self::TABLE,'*',$where,__LINE__,__FILE__) as $row) - { - if (self::DEBUG) - { - _debug_array($row); - } - return $row; + try { + return self::$db->select(self::TABLE,'*',$where,__LINE__,__FILE__)->fetch(ADODB_FETCH_ASSOC); } - return False; + // catch Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' (1267) + // caused by non-ascii chars compared with ascii field uid + catch(egw_exception_db $e) { + _egw_log_exception($e); + } + return false; } /** * Remove link with $link_id or all links matching given params * * @param $link_id link-id to remove if > 0 - * @param string $app='' app-name of links to remove - * @param string $id='' id in $app or '' remove all links from $app - * @param int $owner=0 account_id to delete all links of a given owner, or 0 - * @param string $app2='' appname of 2. endpoint of the link - * @param string $id2='' id in $app2 + * @param string $app ='' app-name of links to remove + * @param string $id ='' id in $app or '' remove all links from $app + * @param int $owner =0 account_id to delete all links of a given owner, or 0 + * @param string $app2 ='' appname of 2. endpoint of the link + * @param string $id2 ='' id in $app2 * @param boolean $hold_for_purge Don't really delete the link, just mark it as deleted and wait for final delete of linked entry * @return array with deleted links */ @@ -319,8 +325,8 @@ class solink /** * Restore links being held as deleted waiting for purge of linked record (un-delete) * - * @param string $app='' app-name of links to remove - * @param string $id='' id in $app or '' remove all links from $app + * @param string $app ='' app-name of links to remove + * @param string $id ='' id in $app or '' remove all links from $app */ static function restore($app, $id) { @@ -381,10 +387,10 @@ class solink * * @param string $app app the returned links are linked on one side (atm. this must be link_app1!) * @param string $target_app app the returned links other side link also to - * @param string|array $target_id=null id(s) the returned links other side link also to - * @param boolean $just_app_ids=false return array with link_id => app_id pairs, not the full link record - * @param string $order='link_lastmod DESC' defaults to newest links first - * @param int|array $limit=null number of entries to return, default null = all or array(offset, num_rows) to return num_rows starting from offset + * @param string|array $target_id =null id(s) the returned links other side link also to + * @param boolean $just_app_ids =false return array with link_id => app_id pairs, not the full link record + * @param string $order ='link_lastmod DESC' defaults to newest links first + * @param int|array $limit =null number of entries to return, default null = all or array(offset, num_rows) to return num_rows starting from offset * @return array with links from entries from $app to $target_app/$target_id plus the other (b) link_id/app/id in the keys 'link3'/'app3'/'id3' */ static function get_3links($app, $target_app, $target_id=null, $just_app_ids=false, $order='link_lastmod DESC', $limit=null) From 0f72e3d58d851c35c82202151452c79dbc6436cf Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 23 Jul 2015 11:49:41 +0000 Subject: [PATCH 020/115] an other one: fix sql error if link-id (accidently) contains non-ascii chars --- phpgwapi/inc/class.solink.inc.php | 33 +++++++++++++++++++------------ 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/phpgwapi/inc/class.solink.inc.php b/phpgwapi/inc/class.solink.inc.php index 3f0f5545b8..527ebba8a6 100644 --- a/phpgwapi/inc/class.solink.inc.php +++ b/phpgwapi/inc/class.solink.inc.php @@ -303,20 +303,27 @@ class solink } } $deleted = array(); - foreach(self::$db->select(self::TABLE,'*',$where,__LINE__,__FILE__) as $row) - { - $deleted[] = $row; + try { + foreach(self::$db->select(self::TABLE,'*',$where,__LINE__,__FILE__) as $row) + { + $deleted[] = $row; + } + if($hold_for_purge) + { + self::$db->update(self::TABLE,array( + 'deleted' => time(), + 'link_lastmod' => time(), + ), $where, __LINE__,__FILE__); + } + else + { + self::$db->delete(self::TABLE,$where,__LINE__,__FILE__); + } } - if($hold_for_purge) - { - self::$db->update(self::TABLE,array( - 'deleted' => time(), - 'link_lastmod' => time(), - ), $where, __LINE__,__FILE__); - } - else - { - self::$db->delete(self::TABLE,$where,__LINE__,__FILE__); + // catch Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' (1267) + // caused by non-ascii chars compared with ascii field uid + catch(egw_exception_db $e) { + _egw_log_exception($e); } return $deleted; From 47239b3d2352a913e10ebc8f1408351127b1ffbe Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Thu, 23 Jul 2015 13:26:43 +0000 Subject: [PATCH 021/115] More W.I.P of mail tree --- mail/inc/class.mail_tree.inc.php | 74 ++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/mail/inc/class.mail_tree.inc.php b/mail/inc/class.mail_tree.inc.php index 256f97eeee..78c4d95f2d 100644 --- a/mail/inc/class.mail_tree.inc.php +++ b/mail/inc/class.mail_tree.inc.php @@ -114,11 +114,14 @@ class mail_tree /** * getTree provides tree structure regarding to selected node * - * @param string $_parent = null, no parent node means root with the first level of folders - * @param string $_profileID icServer id + * @param string $_parent = null no parent node means root with the first level of folders + * @param string $_profileID = '' icServer id + * @param int|boolean $_openTopLevel = 1 Open top level folders on load if it's set to 1|true, + * false|0 leaves them in closed state + * * @return array returns an array of mail tree structure according to provided node */ - function getTree ($_parent = null, $_profileID = '') + function getTree ($_parent = null, $_profileID = '', $_openTopLevel = 1) { //Init mail folders $tree = array(tree::ID=> $_parent?$_parent:0,tree::CHILDREN => array()); @@ -181,16 +184,18 @@ class mail_tree { if (!$accObj->is_imap()|| $acc_id != $_profileID) continue; $identity = emailadmin_account::identity_name($accObj,true,$GLOBALS['egw_info']['user']['acount_id']); - $baseNode = array('id' => $acc_id, - 'text' => str_replace(array('<','>'),array('[',']'),$identity), - 'tooltip' => '('.$acc_id.') '.htmlspecialchars_decode($identity), - 'im0' => self::$leafImages['folderAccount'], - 'im1' => self::$leafImages['folderAccount'], - 'im2' => self::$leafImages['folderAccount'], + $baseNode = array( + tree::ID=> $acc_id, + tree::LABEL => str_replace(array('<','>'),array('[',']'),$identity), + tree::TOOLTIP => '('.$acc_id.') '.htmlspecialchars_decode($identity), + tree::IMAGE_LEAF => self::$leafImages['folderAccount'], + tree::IMAGE_FOLDER_OPEN => self::$leafImages['folderAccount'], + tree::IMAGE_FOLDER_CLOSED => self::$leafImages['folderAccount'], 'path'=> array($acc_id), - 'child'=> true, // dynamic loading on unfold + tree::CHILDREN => array(), // dynamic loading on unfold + tree::AUTOLOAD_CHILDREN => true, 'parent' => '', - 'open' => 1, + tree::OPEN => $_openTopLevel, // mark on account if Sieve is enabled 'data' => array( 'sieve' => $accObj->imapServer()->acc_sieve_enabled, @@ -201,40 +206,43 @@ class mail_tree } //List of folders $foldersList = $this->ui->mail_bo->getFolderArray(null, true); - $nameSpaces = $this->ui->mail_bo->_getNameSpaces(); + // Parent node arrays $parentNode = $parentNodes = array(); - foreach ($nameSpaces as &$nameSpace) + foreach ($foldersList as $index => $topFolder) { - list($index) = explode($nameSpace['delimiter'], $nameSpace['prefix']); - $parentNode = array( - tree::ID=>$_profileID.self::$delimiter.$foldersList[$index]['MAILBOX'], - tree::AUTOLOAD_CHILDREN => $fn_nodeHasChildren($foldersList[$index]), + tree::ID=>$_profileID.self::$delimiter.$topFolder[$index]['MAILBOX'], + tree::AUTOLOAD_CHILDREN => $fn_nodeHasChildren($topFolder[$index]), tree::CHILDREN =>array(), - tree::LABEL =>lang($foldersList[$index]['MAILBOX']), - tree::OPEN => 1, - tree::TOOLTIP => lang($foldersList[$index]['MAILBOX']), - tree::CHECKED => $node['SUBSCRIBED'] + tree::LABEL =>lang($topFolder[$index]['MAILBOX']), + tree::OPEN => $_openTopLevel, + tree::TOOLTIP => lang($topFolder[$index]['MAILBOX']), + tree::CHECKED => $topFolder[$index]['SUBSCRIBED'] ); if ($index === "INBOX") { - $parentNode[tree::IMAGE_LEAF] = self::$leafImages['folderLeaf']; - $parentNode[tree::IMAGE_FOLDER_OPEN] = self::$leafImages['folderOpen']; - $parentNode[tree::IMAGE_FOLDER_CLOSED] = self::$leafImages['folderClose']; + $parentNode[tree::IMAGE_LEAF] = self::$leafImages['folderHome']; + $parentNode[tree::IMAGE_FOLDER_OPEN] = self::$leafImages['folderHome']; + $parentNode[tree::IMAGE_FOLDER_CLOSED] = self::$leafImages['folderHome']; + } + if(stripos(array2string($topFolder[$index]['ATTRIBUTES']),'\noselect')!== false) + { + $parentNode[tree::IMAGE_LEAF] = self::$leafImages['folderNoSelectClosed']; + $parentNode[tree::IMAGE_FOLDER_OPEN] = self::$leafImages['folderNoSelectOpen']; + $parentNode[tree::IMAGE_FOLDER_CLOSED] = self::$leafImages['folderNoSelectClosed']; } // Save parentNodes $parentNodes []= $index; // Remove the parent nodes from the list - unset ($foldersList[$index]); + unset ($topFolder[$index]); //$parentNode[tree::CHILDREN][] =$childrenNode; $baseNode[tree::CHILDREN][] = $parentNode; } foreach ($parentNodes as $pIndex => $parent) { - $indxPattern = '/^'.$parent.'/'; $childrenNodes = $childNode = array(); $definedFolders = array( 'Trash' => $this->ui->mail_bo->getTrashFolder(false), @@ -244,12 +252,16 @@ class mail_tree 'Junk' => $this->ui->mail_bo->getJunkFolder(false), 'Outbox' => $this->ui->mail_bo->getOutboxFolder(false), ); - foreach ($foldersList as &$node) + // Iterate over childern of each top folder(namespaces) + foreach ($foldersList[$parent] as &$node) { + // Skipe the parent node itself + if (is_array($foldersList[$parent][$parent]) && + $foldersList[$parent][$parent]['MAILBOX'] === $node['MAILBOX']) continue; + $pathArr = explode($node['delimiter'], $node['MAILBOX']); $folderName = array_pop($pathArr); $parentPath = $_profileID.self::$delimiter.implode($pathArr,$node['delimiter']); - if (!preg_match($indxPattern, $node['MAILBOX'])) continue; $nodeId = $_profileID.self::$delimiter.$node['MAILBOX']; @@ -269,6 +281,12 @@ class mail_tree $childNode[tree::IMAGE_FOLDER_OPEN] = $childNode [tree::IMAGE_FOLDER_CLOSED] = "MailFolder".$folderName.".png"; } + elseif(stripos(array2string($node['ATTRIBUTES']),'\noselect')!== false) + { + $childNode[tree::IMAGE_LEAF] = self::$leafImages['folderNoSelectClosed']; + $childNode[tree::IMAGE_FOLDER_OPEN] = self::$leafImages['folderNoSelectOpen']; + $childNode[tree::IMAGE_FOLDER_CLOSED] = self::$leafImages['folderNoSelectClosed']; + } else { $childNode[tree::IMAGE_LEAF] = self::$leafImages['folderLeaf']; From 01391055dc20dff843e7ef7b9dd80dbc1dcb256d Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Thu, 23 Jul 2015 13:45:57 +0000 Subject: [PATCH 022/115] W.I.P mail tree: fix wrong profileID --- mail/inc/class.mail_tree.inc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mail/inc/class.mail_tree.inc.php b/mail/inc/class.mail_tree.inc.php index 78c4d95f2d..1b8f7c945a 100644 --- a/mail/inc/class.mail_tree.inc.php +++ b/mail/inc/class.mail_tree.inc.php @@ -135,8 +135,8 @@ class mail_tree return $hasChildren; }; - if ($_parent) $_profileID = $this->ui->mail_bo->profileID; - + if ($_parent) list($_profileID) = explode(self::$delimiter, $_parent); + if (is_numeric($_profileID) && $_profileID != $this->ui->mail_bo->profileID) { try From 2b1d3f63f32a3dd4b6d61c55e1092aeb3bb80716 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 23 Jul 2015 14:56:20 +0000 Subject: [PATCH 023/115] * Univention: support Dovecot as IMAP available and default from 4.0-2 on --- doc/rpm-build/post_install.php | 18 +++++++++++++++--- phpgwapi/inc/class.accounts_univention.inc.php | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/doc/rpm-build/post_install.php b/doc/rpm-build/post_install.php index 3869d729a9..85c0605004 100755 --- a/doc/rpm-build/post_install.php +++ b/doc/rpm-build/post_install.php @@ -649,15 +649,27 @@ function set_univention_defaults() $config['smtpserver'] = "$mailserver,465,,,yes,tls,no,yes"; $config['smtp'] = ',emailadmin_smtp_univention'; $config['mailserver'] = "$mailserver,993,$domain,email,tls"; - $config['imap'] = /*'cyrus,'._ucr_secret('cyrus')*/','.',emailadmin_imap_cyrus'; + if (_ucr_get('mail/dovecot') == 'yes') + { + $config['imap'] = /*'cyrus,'._ucr_secret('cyrus')*/','.',emailadmin_imap_dovecot'; + // default with sieve port to 4190, as config is only available on host mailserver app is installed + if (!($sieve_port = _ucr_get('mail/dovecot/sieve/port'))) $sieve_port = 4190; + } + else + { + $config['imap'] = /*'cyrus,'._ucr_secret('cyrus')*/','.',emailadmin_imap_cyrus'; + // default with sieve port to 4190, as config is only available on host mailserver app is installed + if (!($sieve_port = _ucr_get('mail/cyrus/sieve/port'))) $sieve_port = 4190; + } // set folders so mail creates them on first login, UCS does not automatic $config['folder'] = 'INBOX/Sent,INBOX/Trash,INBOX/Drafts,INBOX/Templates,INBOX/Spam'; - // default with sieve port to 4190, as config is only available on host mailserver app is installed - if (!($sieve_port = _ucr_get('mail/cyrus/sieve/port'))) $sieve_port = 4190; $config['sieve'] = "$mailserver,$sieve_port,starttls"; // set an email address for sysop user so mail works right away $config['admin_email'] = '$admin_user@'.$domain; } + # add directory of univention-directory-manager and it's sysmlink target to open_basedir + system("/bin/sed -i 's|/usr/bin|/usr/bin:/usr/sbin:/usr/share/univention-directory-manager-tools|' /etc/egroupware/apache.conf"); + } } diff --git a/phpgwapi/inc/class.accounts_univention.inc.php b/phpgwapi/inc/class.accounts_univention.inc.php index dd6006d2d8..669342b975 100644 --- a/phpgwapi/inc/class.accounts_univention.inc.php +++ b/phpgwapi/inc/class.accounts_univention.inc.php @@ -27,6 +27,9 @@ class accounts_univention extends accounts_ldap /** * Name of binary to call + * + * It is a symlink to /usr/share/univention-directory-manager-tools/directory-manager-cli. + * Both directories must be included in open_basedir! */ const DIRECTORY_MANAGER_BIN = '/usr/sbin/univention-directory-manager'; @@ -60,6 +63,21 @@ class accounts_univention extends accounts_ldap if (!empty($data['account_passwd'])) { $params[] = '--set'; $params[] = 'password='.$data['account_passwd']; + // we need to set mailHomeServer, so mailbox gets created for Dovecot + // get_default() does not work for Adminstrator, try acc_id=1 instead + // if everything fails try hostname ... + try { + if (!($account = emailadmin_account::get_default())) + { + $account = emailadmin_account::read(1); + } + $hostname = $account->acc_imap_host; + } + catch(Exception $e) { + unset($e); + } + if (empty($hostname)) $hostname = trunc(system('hostname -f')); + $params[] = '--set'; $params[] = 'mailHomeServer='.$hostname; } $cmd = self::DIRECTORY_MANAGER_BIN.' '.implode(' ', array_map('escapeshellarg', $params)); $output_arr = $ret = $matches = null; From 2fcde50257d4e5e13d2f6da902d161dbbfd825aa Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 23 Jul 2015 15:51:26 +0000 Subject: [PATCH 024/115] * CalDAV/Calendar/InfoLog: do NOT use ENCODING=QUOTED-PRINTABLE for iCal 2.0, cuts eg. description off in TB, if containing non-ascii chars --- calendar/inc/class.calendar_ical.inc.php | 7 +++---- infolog/inc/class.infolog_ical.inc.php | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/calendar/inc/class.calendar_ical.inc.php b/calendar/inc/class.calendar_ical.inc.php index ba6930a2a4..2d4f7acd10 100644 --- a/calendar/inc/class.calendar_ical.inc.php +++ b/calendar/inc/class.calendar_ical.inc.php @@ -987,8 +987,8 @@ class calendar_ical extends calendar_boupdate translation::charset(),$charset); $content = $valueData . implode(';', $valuesData); - if (preg_match('/[^\x20-\x7F]/', $content) || - ($paramData['CN'] && preg_match('/[^\x20-\x7F]/', $paramData['CN']))) + if ($version == '1.0' && (preg_match('/[^\x20-\x7F]/', $content) || + ($paramData['CN'] && preg_match('/[^\x20-\x7F]/', $paramData['CN'])))) { $paramData['CHARSET'] = $charset; switch ($this->productManufacturer) @@ -998,7 +998,6 @@ class calendar_ical extends calendar_boupdate { $paramData['ENCODING'] = 'QUOTED-PRINTABLE'; } - /* disable automatic QP encoding eg. for new-lines as it also encodes non-ascii/utf-8 which TB does NOT decode else { $paramData['CHARSET'] = ''; @@ -1010,7 +1009,7 @@ class calendar_ical extends calendar_boupdate { $paramData['ENCODING'] = ''; } - }*/ + } break; case 'funambol': $paramData['ENCODING'] = 'FUNAMBOL-QP'; diff --git a/infolog/inc/class.infolog_ical.inc.php b/infolog/inc/class.infolog_ical.inc.php index 365d395245..261c8d2934 100644 --- a/infolog/inc/class.infolog_ical.inc.php +++ b/infolog/inc/class.infolog_ical.inc.php @@ -281,7 +281,7 @@ class infolog_ical extends infolog_bo $options = array(); } - if (preg_match('/[^\x20-\x7F]/', $value)) + if ($_version == '1.0' && preg_match('/[^\x20-\x7F]/', $value)) { $options['CHARSET'] = $charset; switch ($this->productManufacturer) From f1ba35613cbac1c4c1a1a1dd772b35bcf7c6c46f Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 23 Jul 2015 17:40:48 +0000 Subject: [PATCH 025/115] * CardDAV/Addressbook: fix iOS 8.4 problem allways creating new contacts for admins in accounts addressbook, using now default or personal addressbook --- addressbook/inc/class.addressbook_bo.inc.php | 10 ++++++++-- .../inc/class.addressbook_groupdav.inc.php | 15 +++++++++++---- addressbook/inc/class.addressbook_ui.inc.php | 5 +++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/addressbook/inc/class.addressbook_bo.inc.php b/addressbook/inc/class.addressbook_bo.inc.php index 5355b9c938..2d0e84b4bf 100755 --- a/addressbook/inc/class.addressbook_bo.inc.php +++ b/addressbook/inc/class.addressbook_bo.inc.php @@ -7,7 +7,7 @@ * @author Ralf Becker * @author Joerg Lehrke * @package addressbook - * @copyright (c) 2005-12 by Ralf Becker + * @copyright (c) 2005-15 by Ralf Becker * @copyright (c) 2005/6 by Cornelius Weiss * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ @@ -367,7 +367,8 @@ class addressbook_bo extends addressbook_so asort($to_sort); $addressbooks += $to_sort; } - if (!$preferences['addressbook']['hide_accounts'] && ( + if ($required != EGW_ACL_ADD && // do NOT allow to set accounts as default addressbook (AB can add accounts) + !$preferences['addressbook']['hide_accounts'] && ( ($grants[0] & $required) == $required || $preferences['common']['account_selection'] == 'groupmembers' && $this->account_repository != 'ldap' && ($required & EGW_ACL_READ))) @@ -862,6 +863,11 @@ class addressbook_bo extends addressbook_so // if no owner/addressbook set use the setting of the add_default prefs (if set, otherwise the users personal addressbook) if (!isset($contact['owner'])) $contact['owner'] = $this->default_addressbook; if (!isset($contact['private'])) $contact['private'] = (int)$this->default_private; + // do NOT allow to create new accounts via addressbook, they are broken without an account_id + if (!$contact['owner'] && empty($contact['account_id'])) + { + $contact['owner'] = $this->default_addressbook ? $this->default_addressbook : $this->user; + } // allow admins to import contacts with creator / created date set if (!$contact['creator'] || !$this->is_admin($contact)) $contact['creator'] = $this->user; if (!$contact['created'] || !$this->is_admin($contact)) $contact['created'] = $this->now_su; diff --git a/addressbook/inc/class.addressbook_groupdav.inc.php b/addressbook/inc/class.addressbook_groupdav.inc.php index fcd6403be3..d32b06965e 100644 --- a/addressbook/inc/class.addressbook_groupdav.inc.php +++ b/addressbook/inc/class.addressbook_groupdav.inc.php @@ -643,12 +643,12 @@ class addressbook_groupdav extends groupdav_handler $contact['carddav_name'] = $id; // only set owner, if user is explicitly specified in URL (check via prefix, NOT for /addressbook/) or sync-all-in-one!) - if ($prefix && !in_array('O',$this->home_set_pref)) + if ($prefix && !in_array('O',$this->home_set_pref) && $user) { $contact['owner'] = $user; } - // check if default addressbook is synced, if not use (always synced) personal addressbook - elseif($this->bo->default_addressbook && !in_array($this->bo->default_addressbook,$this->home_set_pref)) + // check if default addressbook is synced and not accounts, if not use (always synced) personal addressbook + elseif(!$this->bo->default_addressbook || !in_array($this->bo->default_addressbook,$this->home_set_pref)) { $contact['owner'] = $GLOBALS['egw_info']['user']['account_id']; } @@ -1027,7 +1027,14 @@ class addressbook_groupdav extends groupdav_handler */ public function get_grants() { - return $this->bo->get_grants($this->bo->user); + $grants = $this->bo->get_grants($this->bo->user); + + // remove add and delete grants for accounts (for admins too) + // as accounts can not be created as contacts, they eg. need further data + // and admins might not recognice they delete an account incl. its data + if (isset($grants[0])) $grants[0] &= ~(EGW_ACL_ADD|EGW_ACL_DELETE); + + return $grants; } /** diff --git a/addressbook/inc/class.addressbook_ui.inc.php b/addressbook/inc/class.addressbook_ui.inc.php index 89e143b345..f27778a27b 100644 --- a/addressbook/inc/class.addressbook_ui.inc.php +++ b/addressbook/inc/class.addressbook_ui.inc.php @@ -1129,10 +1129,11 @@ window.egw_LAB.wait(function() { if (($Ok = !!($contact = $this->read($id)) && $this->check_perms(EGW_ACL_DELETE,$contact))) { if ($contact['owner'] || // regular contact or + empty($contact['account_id']) || // accounts without account_id // already deleted account (should no longer happen, but needed to allow for cleanup) $contact['tid'] == addressbook_so::DELETED_TYPE) { - $Ok = $this->delete($id, $contact['tid'] != addressbook_so::DELETED_TYPE); + $Ok = $this->delete($id, $contact['tid'] != addressbook_so::DELETED_TYPE && $contact['account_id']); } // delete single account --> redirect to admin elseif (count($checked) == 1 && $contact['account_id']) @@ -1641,7 +1642,7 @@ window.egw_LAB.wait(function() { { $row['tel_prefered'] = $row[$row['tel_prefer']].$prefer_marker; } - if (!$row['owner']) + if (!$row['owner'] && $row['account_id'] > 0) { $row['class'] .= 'rowAccount rowNoDelete '; } From 0cf1bd1452a3d6d856e2524231237121351485b2 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Fri, 24 Jul 2015 11:42:55 +0000 Subject: [PATCH 026/115] fix nothing to change found, if admin-cli was called with root_admin and config-password --- .../class.admin_cmd_change_account_id.inc.php | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/admin/inc/class.admin_cmd_change_account_id.inc.php b/admin/inc/class.admin_cmd_change_account_id.inc.php index f78a9bd942..145ebb4243 100644 --- a/admin/inc/class.admin_cmd_change_account_id.inc.php +++ b/admin/inc/class.admin_cmd_change_account_id.inc.php @@ -5,7 +5,7 @@ * @link http://www.egroupware.org * @author Ralf Becker * @package admin - * @copyright (c) 2007-13 by Ralf Becker + * @copyright (c) 2007-15 by Ralf Becker * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ @@ -41,8 +41,14 @@ class admin_cmd_change_account_id extends admin_cmd */ private function get_changes() { - $changes = array(); - foreach($GLOBALS['egw_info']['apps'] as $app => $app_data) + // happens if one used "root_admin" and config-password + if (empty($GLOBALS['egw_info']['apps'])) + { + $apps = new applications(); + $apps->read_installed_apps(); + } + $changes = $setup_info = array(); + foreach(array_keys($GLOBALS['egw_info']['apps']) as $app) { if (!file_exists($path=EGW_SERVER_ROOT.'/'.$app.'/setup/setup.inc.php') || !include($path)) continue; @@ -57,8 +63,7 @@ class admin_cmd_change_account_id extends admin_cmd { foreach((array)$data['meta'] as $key => $val) { - unset($subtype); - list($type, $subtype) = explode('-', $val); + list($type, $subtype) = explode('-', $val.'-'); if (in_array($type, array('account', 'user', 'group'))) { if (!is_numeric($key) || !empty($subtype)) @@ -101,7 +106,7 @@ class admin_cmd_change_account_id extends admin_cmd /** * give or remove run rights from a given account and application * - * @param boolean $check_only=false only run the checks (and throw the exceptions), but not the command itself + * @param boolean $check_only =false only run the checks (and throw the exceptions), but not the command itself * @return string success message * @throws egw_exception_no_admin * @throws egw_exception_wrong_userinput(lang("Unknown account: %1 !!!",$this->account),15); @@ -200,8 +205,6 @@ class admin_cmd_change_account_id extends admin_cmd $update_sql .= "WHEN ".$db->quote($from,$db->column_definitions[$column]['type'])." THEN ".$db->quote($to,$db->column_definitions[$column]['type'])." "; } $update_sql .= 'END'; - $update_sql_prefs .= 'END'; - if ($update_sql_abs) $update_sql_abs .= 'END'; switch($type) { @@ -214,15 +217,15 @@ class admin_cmd_change_account_id extends admin_cmd $change = array(); foreach($db->select($table,'DISTINCT '.$column,$select,__LINE__,__FILE__) as $row) { - $ids = $type != 'serialized' ? explode(',',$old_ids=$row[$column]) : unserialize($old_ids=$row[$column]); + $ids = $type != 'serialized' ? explode(',',$old_ids=$row[$column]) : json_php_unserialize($old_ids=$row[$column]); foreach($ids as $key => $id) { if (isset($ids2change[$id])) $ids[$key] = $ids2change[$id]; } - $ids = $type != 'serialized' ? implode(',',$ids) : serialize($ids); - if ($ids != $old_ids) + $ids2 = $type != 'serialized' ? implode(',',$ids) : serialize($ids); + if ($ids2 != $old_ids) { - $change[$old_ids] = $ids; + $change[$old_ids] = $ids2; } } $changed = 0; @@ -277,8 +280,10 @@ class admin_cmd_change_account_id extends admin_cmd function __tostring() { $change = array(); - foreach($this->change as $from => $to) $change[] = $from.'->'.$to; - + foreach($this->change as $from => $to) + { + $change[] = $from.'->'.$to; + } return lang('Change account_id').': '.implode(', ',$change); } } From 5ef86e0b16bdfb4a7502723930ee6292caa4b4be Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Fri, 24 Jul 2015 12:06:33 +0000 Subject: [PATCH 027/115] change history_status back to varchar, as it contains custom-field names, which can be non-ascii --- phpgwapi/setup/setup.inc.php | 3 ++- phpgwapi/setup/tables_current.inc.php | 2 +- phpgwapi/setup/tables_update.inc.php | 19 ++++++++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/phpgwapi/setup/setup.inc.php b/phpgwapi/setup/setup.inc.php index 1d8a007986..4f43bc305e 100755 --- a/phpgwapi/setup/setup.inc.php +++ b/phpgwapi/setup/setup.inc.php @@ -12,7 +12,7 @@ /* Basic information about this app */ $setup_info['phpgwapi']['name'] = 'phpgwapi'; $setup_info['phpgwapi']['title'] = 'EGroupware API'; -$setup_info['phpgwapi']['version'] = '14.3'; +$setup_info['phpgwapi']['version'] = '14.3.001'; $setup_info['phpgwapi']['versions']['current_header'] = '1.29'; $setup_info['phpgwapi']['enable'] = 3; $setup_info['phpgwapi']['app_order'] = 1; @@ -73,3 +73,4 @@ $setup_info['groupdav']['author'] = $setup_info['groupdav']['maintainer'] = arra $setup_info['groupdav']['license'] = 'GPL'; $setup_info['groupdav']['hooks']['preferences'] = 'groupdav_hooks::menus'; $setup_info['groupdav']['hooks']['settings'] = 'groupdav_hooks::settings'; + diff --git a/phpgwapi/setup/tables_current.inc.php b/phpgwapi/setup/tables_current.inc.php index c268b05ae7..51aefca55d 100644 --- a/phpgwapi/setup/tables_current.inc.php +++ b/phpgwapi/setup/tables_current.inc.php @@ -173,7 +173,7 @@ $phpgw_baseline = array( 'history_record_id' => array('type' => 'int','precision' => '4','nullable' => False), 'history_appname' => array('type' => 'ascii','precision' => '16','nullable' => False), 'history_owner' => array('type' => 'int','meta' => 'user','precision' => '4','nullable' => False), - 'history_status' => array('type' => 'ascii','precision' => '32','nullable' => False), + 'history_status' => array('type' => 'varchar','precision' => '32','nullable' => False), 'history_new_value' => array('type' => 'text','nullable' => False), 'history_timestamp' => array('type' => 'timestamp','nullable' => False,'default' => 'current_timestamp'), 'history_old_value' => array('type' => 'text','nullable' => False), diff --git a/phpgwapi/setup/tables_update.inc.php b/phpgwapi/setup/tables_update.inc.php index 9f5eef5888..55f65090e6 100644 --- a/phpgwapi/setup/tables_update.inc.php +++ b/phpgwapi/setup/tables_update.inc.php @@ -375,7 +375,7 @@ function phpgwapi_upgrade14_2_013() 'nullable' => False )); $GLOBALS['egw_setup']->oProc->AlterColumn('egw_history_log','history_status',array( - 'type' => 'ascii', + 'type' => 'varchar', 'precision' => '32', 'nullable' => False )); @@ -781,3 +781,20 @@ function phpgwapi_upgrade14_2_026() return $GLOBALS['setup_info']['phpgwapi']['currentver'] = '14.3'; } + +/** + * Change history_status back to varchar, as it contains custom-field names, which can be non-ascii + * + * @return string + */ +function phpgwapi_upgrade14_3() +{ + $GLOBALS['egw_setup']->oProc->AlterColumn('egw_history_log','history_status',array( + 'type' => 'varchar', + 'precision' => '32', + 'nullable' => False + )); + + return $GLOBALS['setup_info']['phpgwapi']['currentver'] = '14.3.001'; +} + From eb2ec31a4e82103e416b964be563230b5812ef4b Mon Sep 17 00:00:00 2001 From: Klaus Leithoff Date: Fri, 24 Jul 2015 12:08:21 +0000 Subject: [PATCH 028/115] do not use SYNC_GAL_ALIAS within the result-set when not requested; as we do not request it. omit it completely --- addressbook/inc/class.addressbook_zpush.inc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addressbook/inc/class.addressbook_zpush.inc.php b/addressbook/inc/class.addressbook_zpush.inc.php index 709246c945..fe19604503 100644 --- a/addressbook/inc/class.addressbook_zpush.inc.php +++ b/addressbook/inc/class.addressbook_zpush.inc.php @@ -803,8 +803,8 @@ class addressbook_zpush implements activesync_plugin_write, activesync_plugin_se { foreach($contacts as $contact) { - $item[SYNC_GAL_ALIAS] = $contact['contact_id']; - $item[SYNC_GAL_LASTNAME] = $contact['n_family']; + //$item[SYNC_GAL_ALIAS] = $contact['contact_id']; + $item[SYNC_GAL_LASTNAME] = $contact['n_family']?$contact['n_family']:$contact['org_name']; $item[SYNC_GAL_FIRSTNAME] = $contact['n_given']; $item[SYNC_GAL_DISPLAYNAME] = $contact['n_fn']; if (!trim($item[SYNC_GAL_DISPLAYNAME])) $item[SYNC_GAL_DISPLAYNAME] = $contact['n_family']?$contact['n_family']:$contact['org_name']; From 310c241ca4e9cb31b12e68e7cb83f2435e6f4cda Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Fri, 24 Jul 2015 13:33:27 +0000 Subject: [PATCH 029/115] Send back to server both checked and unchecked values if autoloading is used --- etemplate/js/et2_widget_tree.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/etemplate/js/et2_widget_tree.js b/etemplate/js/et2_widget_tree.js index abd4238370..306880d3a2 100644 --- a/etemplate/js/et2_widget_tree.js +++ b/etemplate/js/et2_widget_tree.js @@ -442,7 +442,30 @@ var et2_tree = et2_inputWidget.extend( */ getValue: function() { if(this.input == null) return null; - return this.options.multiple ? this.input.getAllChecked().split(this.input.dlmtr) : this.input.getSelectedItemId(); + if (this.options.multiple) + { + var allChecked = this.input.getAllChecked().split(this.input.dlmtr); + var allUnchecked = this.input.getAllUnchecked().split(this.input.dlmtr); + if (this.options.autoloading) + { + + var res = {}; + for (var i=0;i Date: Mon, 27 Jul 2015 13:17:03 +0000 Subject: [PATCH 030/115] if db-object passed to so_sql_cf, use that also for querying custom-fields --- etemplate/inc/class.so_sql_cf.inc.php | 2 +- phpgwapi/inc/class.egw_customfields.inc.php | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/etemplate/inc/class.so_sql_cf.inc.php b/etemplate/inc/class.so_sql_cf.inc.php index 824751c92b..0b17ad6aac 100644 --- a/etemplate/inc/class.so_sql_cf.inc.php +++ b/etemplate/inc/class.so_sql_cf.inc.php @@ -164,7 +164,7 @@ class so_sql_cf extends so_sql $this->extra_join_order = " LEFT JOIN $extra_table extra_order ON $table.$this->autoinc_id=extra_order.$this->extra_id"; $this->extra_join_filter = " JOIN $extra_table extra_filter ON $table.$this->autoinc_id=extra_filter.$this->extra_id"; - $this->customfields = egw_customfields::get($app); + $this->customfields = egw_customfields::get($app, false, null, $db); } /** diff --git a/phpgwapi/inc/class.egw_customfields.inc.php b/phpgwapi/inc/class.egw_customfields.inc.php index 622f0c7c64..c320994960 100755 --- a/phpgwapi/inc/class.egw_customfields.inc.php +++ b/phpgwapi/inc/class.egw_customfields.inc.php @@ -55,9 +55,10 @@ class egw_customfields implements IteratorAggregate * @param string $only_type2 =null if given only return fields of type2 == $only_type2 * @param int $start =0 * @param int $num_rows =null + * @param egw_db $db =null reference to database instance to use * @return array with customfields */ - function __construct($app, $all_private_too=false, $only_type2=null, $start=0, $num_rows=null) + function __construct($app, $all_private_too=false, $only_type2=null, $start=0, $num_rows=null, egw_db $db=null) { $this->app = $app; $this->all_private_too = $all_private_too; @@ -75,7 +76,8 @@ class egw_customfields implements IteratorAggregate { $query[] = $this->commasep_match('cf_type2', $only_type2); } - $this->iterator = self::$db->select(self::TABLE, '*', $query, __LINE__, __FILE__, + if (!$db) $db = self::$db; + $this->iterator = $db->select(self::TABLE, '*', $query, __LINE__, __FILE__, !isset($num_rows) ? false : $start, 'ORDER BY cf_order ASC', 'phpgwapi', $num_rows); } @@ -123,16 +125,17 @@ class egw_customfields implements IteratorAggregate * @param string $app * @param boolean $all_private_too =false should all the private fields be returned too, default no * @param string $only_type2 =null if given only return fields of type2 == $only_type2 + * @param egw_db $db =null reference to database to use * @return array with customfields */ - public static function get($app, $all_private_too=false, $only_type2=null) + public static function get($app, $all_private_too=false, $only_type2=null, egw_db $db=null) { $cache_key = $app.':'.($all_private_too?'all':$GLOBALS['egw_info']['user']['account_id']).':'.$only_type2; $cfs = egw_cache::getInstance(__CLASS__, $cache_key); if (!isset($cfs)) { - $cfs = iterator_to_array(new egw_customfields($app, $all_private_too, $only_type2)); + $cfs = iterator_to_array(new egw_customfields($app, $all_private_too, $only_type2, 0, null, $db)); egw_cache::setInstance(__CLASS__, $cache_key, $cfs); $cached = egw_cache::getInstance(__CLASS__, $app); @@ -401,7 +404,7 @@ class egw_customfields implements IteratorAggregate } } } - + self::$db->$op(self::TABLE, array( 'cf_label' => $cf['label'], 'cf_type' => $cf['type'], From ce9c1187bf937e4eb8ece453ed6974a36f1384ac Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Mon, 27 Jul 2015 13:34:38 +0000 Subject: [PATCH 031/115] Add nocheckbox option into tree widget to instruct the component not to render checkbox for the relevant item --- etemplate/inc/class.etemplate_widget_tree.inc.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/etemplate/inc/class.etemplate_widget_tree.inc.php b/etemplate/inc/class.etemplate_widget_tree.inc.php index 0629f98d80..1712422573 100644 --- a/etemplate/inc/class.etemplate_widget_tree.inc.php +++ b/etemplate/inc/class.etemplate_widget_tree.inc.php @@ -32,7 +32,7 @@ egw_framework::includeCSS('/phpgwapi/js/dhtmlxtree/codebase/dhtmlXTree.css'); * array(tree::ID => '/INBOX/sub', tree::LABEL => 'sub', tree::IMAGE_LEAF => 'folderClosed.gif'), * array(tree::ID => '/INBOX/sub2', tree::LABEL => 'sub2', tree::IMAGE_LEAF => 'folderClosed.gif'), * ), - * tree::CHECKED => true + * tree::CHECKED => true, * ), * array( * tree::ID => '/user', @@ -41,7 +41,7 @@ egw_framework::includeCSS('/phpgwapi/js/dhtmlxtree/codebase/dhtmlXTree.css'); * array(tree::ID => '/user/birgit', tree::LABEL => 'birgit', tree::IMAGE_LEAF => 'folderClosed.gif'), * array(tree::ID => '/user/ralf', tree::LABEL => 'ralf', tree::AUTOLOAD_CHILDREN => 1), * ), - * tree::CHECKED => false + * tree::NOCHECKBOX => true * ), * )); * @@ -99,10 +99,15 @@ class etemplate_widget_tree extends etemplate_widget const OPEN = 'open'; /** - * check checkbox if exists (in case of three-state checkboxes values can be:0 unchecked- 1 - checked or -1 - unsure) + * key to check checkbox if exists (in case of three-state checkboxes values can be:0 unchecked- 1 - checked or -1 - unsure) */ const CHECKED = 'checked'; + /** + * key to instruct the component not to render checkbox for the related item, optional + */ + const NOCHECKBOX = 'nocheckbox'; + /** * Parse and set extra attributes from xml in template object * From ea0663d531f054186a82fb2dd58e1dcb8737c98d Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Mon, 27 Jul 2015 15:24:27 +0000 Subject: [PATCH 032/115] Prevent column selection preference from being reset, fixes forced columns don't stay forced. --- etemplate/inc/class.etemplate_widget_nextmatch.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etemplate/inc/class.etemplate_widget_nextmatch.inc.php b/etemplate/inc/class.etemplate_widget_nextmatch.inc.php index a05da14874..0a87034523 100644 --- a/etemplate/inc/class.etemplate_widget_nextmatch.inc.php +++ b/etemplate/inc/class.etemplate_widget_nextmatch.inc.php @@ -1049,7 +1049,7 @@ class etemplate_widget_nextmatch extends etemplate_widget // Save current column settings as default, clear, or force (admins only) - if($GLOBALS['egw_info']['user']['apps']['admin'] && $app) + if($GLOBALS['egw_info']['user']['apps']['admin'] && $app && $value['selectcols']) { $pref_name = 'nextmatch-' . (isset($content_value['columnselection_pref']) ? $content_value['columnselection_pref'] : $this->attrs['template']); $refresh_pref_name = $pref_name.'-autorefresh'; From af157c3218415abdd81a57adc42dda2351465682 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 27 Jul 2015 16:52:55 +0000 Subject: [PATCH 033/115] * Addressbook: custom fields of accounts enabled via own-account-acl were not editable --- addressbook/inc/class.addressbook_bo.inc.php | 20 +++-- .../inc/class.addressbook_tracking.inc.php | 4 +- ...lass.etemplate_widget_customfields.inc.php | 90 +++++++++---------- etemplate/js/et2_extension_customfields.js | 5 +- 4 files changed, 63 insertions(+), 56 deletions(-) diff --git a/addressbook/inc/class.addressbook_bo.inc.php b/addressbook/inc/class.addressbook_bo.inc.php index 2d0e84b4bf..5fc7df43b1 100755 --- a/addressbook/inc/class.addressbook_bo.inc.php +++ b/addressbook/inc/class.addressbook_bo.inc.php @@ -925,6 +925,11 @@ class addressbook_bo extends addressbook_so } if (isset($contact['org_name'])) $contact['n_fileas'] = $this->fileas($contact, null, false); + // Get old record for tracking changes + if (!isset($old) && $isUpdate) + { + $old = $this->read($contact['id']); + } $to_write = $contact; // (non-admin) user editing his own account, make sure he does not change fields he is not allowed to (eg. via SyncML or xmlrpc) if (!$ignore_acl && !$contact['owner'] && !$this->is_admin($contact)) @@ -933,16 +938,19 @@ class addressbook_bo extends addressbook_so { if (!in_array($field,$this->own_account_acl) && !in_array($field,array('id','owner','account_id','modified','modifier'))) { - unset($to_write[$field]); // user is now allowed to change that + // user is not allowed to change that + if ($old) + { + $to_write[$field] = $old[$field]; + } + else + { + unset($to_write[$field]); + } } } } - // Get old record for tracking changes - if (!isset($old) && $isUpdate) - { - $old = $this->read($contact['id']); - } // IF THE OLD ENTRY IS A ACCOUNT, dont allow to change the owner/location // maybe we need that for id and account_id as well. if (is_array($old) && (!isset($old['owner']) || empty($old['owner']))) diff --git a/addressbook/inc/class.addressbook_tracking.inc.php b/addressbook/inc/class.addressbook_tracking.inc.php index 9187aa233d..62812f97fe 100644 --- a/addressbook/inc/class.addressbook_tracking.inc.php +++ b/addressbook/inc/class.addressbook_tracking.inc.php @@ -150,9 +150,9 @@ class addressbook_tracking extends bo_tracking foreach(array('adr_one_countryname' => 'adr_one_countrycode', 'adr_two_countryname' => 'adr_two_countrycode') as $name => $code) { // Only codes involved, but old text name is automatically added when loaded - if($old[$code] && $data[$code]) + if($old[$code] && $data[$code] && ($key = array_search($name, $changed_fields)) !== false) { - unset($changed_fields[array_search($name, $changed_fields)]); + unset($changed_fields[$key]); continue; } diff --git a/etemplate/inc/class.etemplate_widget_customfields.inc.php b/etemplate/inc/class.etemplate_widget_customfields.inc.php index 8cc0287017..1a7ec2b118 100644 --- a/etemplate/inc/class.etemplate_widget_customfields.inc.php +++ b/etemplate/inc/class.etemplate_widget_customfields.inc.php @@ -347,56 +347,56 @@ class etemplate_widget_customfields extends etemplate_widget_transformer $form_name = self::GLOBAL_ID; } - if (!$this->is_readonly($cname, $form_name)) + $all_readonly = $this->is_readonly($cname, $form_name); + $value_in = self::get_array($content, $form_name); + // if we have no id / use self::GLOBAL_ID, we have to set $value_in in global namespace for regular widgets validation to find + if (!$this->id) $content = array_merge($content, $value_in); + //error_log(__METHOD__."($cname, ...) form_name=$form_name, use-private={$this->attrs['use-private']}, value_in=".array2string($value_in)); + $customfields =& $this->getElementAttribute(self::GLOBAL_VALS, 'customfields'); + if(is_array($value_in)) { - $value_in = self::get_array($content, $form_name); - // if we have no id / use self::GLOBAL_ID, we have to set $value_in in global namespace for regular widgets validation to find - if (!$this->id) $content = array_merge($content, $value_in); - //error_log(__METHOD__."($cname, ...) form_name=$form_name, use-private={$this->attrs['use-private']}, value_in=".array2string($value_in)); - $customfields =& $this->getElementAttribute(self::GLOBAL_VALS, 'customfields'); - if(is_array($value_in)) + foreach($value_in as $field => $value) { - foreach($value_in as $field => $value) + $field_settings = $customfields[$fname=substr($field,1)]; + + if ((string)$this->attrs['use-private'] !== '' && // are only (non-)private fields requested + (boolean)$field_settings['private'] != ($this->attrs['use-private'] != '0')) { - $field_settings = $customfields[$fname=substr($field,1)]; - - if ((string)$this->attrs['use-private'] !== '' && // are only (non-)private fields requested - (boolean)$field_settings['private'] != ($this->attrs['use-private'] != '0')) - { - continue; - } - - // check if single field is set readonly, used in apps as it was only way to make cfs readonly in old eT - if ($this->is_readonly($form_name != self::GLOBAL_ID ? $form_name : $cname, $field)) - { - continue; - } - // run validation method of widget implementing this custom field - $widget = $this->_widget($fname, $field_settings); - // widget has no validate method, eg. is only displaying stuff --> nothing to validate - if (!method_exists($widget, 'validate')) continue; - $widget->validate($form_name != self::GLOBAL_ID ? $form_name : $cname, $expand, $content, $validated); - if ($field_settings['needed'] && (is_array($value) ? !$value : (string)$value === '')) - { - self::set_validation_error($field,lang('Field must not be empty !!!'),''); - } - $field_name = $this->id[0] == self::$prefix && $customfields[substr($this->id,1)] ? $this->id : self::form_name($form_name != self::GLOBAL_ID ? $form_name : $cname, $field); - $valid =& self::get_array($validated, $field_name, true); - - if (is_array($valid)) $valid = implode(',', $valid); - // NULL is valid for most fields, but not custom fields due to backend handling - // See so_sql_cf->save() - if (is_null($valid)) $valid = false; - //error_log(__METHOD__."() $field_name: ".array2string($value).' --> '.array2string($valid)); + continue; } + + // check if single field is set readonly, used in apps as it was only way to make cfs readonly in old eT + // single fields set to false in $readonly overwrite a global __ALL__ + $cf_readonly = $this->is_readonly($form_name != self::GLOBAL_ID ? $form_name : $cname, $field); + if ($cf_readonly || $all_readonly && $cf_readonly !== false) + { + continue; + } + // run validation method of widget implementing this custom field + $widget = $this->_widget($fname, $field_settings); + // widget has no validate method, eg. is only displaying stuff --> nothing to validate + if (!method_exists($widget, 'validate')) continue; + $widget->validate($form_name != self::GLOBAL_ID ? $form_name : $cname, $expand, $content, $validated); + if ($field_settings['needed'] && (is_array($value) ? !$value : (string)$value === '')) + { + self::set_validation_error($field,lang('Field must not be empty !!!'),''); + } + $field_name = $this->id[0] == self::$prefix && $customfields[substr($this->id,1)] ? $this->id : self::form_name($form_name != self::GLOBAL_ID ? $form_name : $cname, $field); + $valid =& self::get_array($validated, $field_name, true); + + if (is_array($valid)) $valid = implode(',', $valid); + // NULL is valid for most fields, but not custom fields due to backend handling + // See so_sql_cf->save() + if (is_null($valid)) $valid = false; + //error_log(__METHOD__."() $field_name: ".array2string($value).' --> '.array2string($valid)); } - elseif ($this->type == 'customfields-types') - { - // Transformation doesn't handle validation - $valid =& self::get_array($validated, $this->id ? $form_name : $field, true); - if (true) $valid = $value_in; - //error_log(__METHOD__."() $form_name $field: ".array2string($value).' --> '.array2string($value)); - } + } + elseif ($this->type == 'customfields-types') + { + // Transformation doesn't handle validation + $valid =& self::get_array($validated, $this->id ? $form_name : $field, true); + if (true) $valid = $value_in; + //error_log(__METHOD__."() $form_name $field: ".array2string($value).' --> '.array2string($value)); } } } diff --git a/etemplate/js/et2_extension_customfields.js b/etemplate/js/et2_extension_customfields.js index 57d3c6eeda..dcbae50553 100644 --- a/etemplate/js/et2_extension_customfields.js +++ b/etemplate/js/et2_extension_customfields.js @@ -200,8 +200,7 @@ var et2_customfields_list = et2_valueWidget.extend([et2_IDetachedDOM, et2_IInput 'id': id, 'statustext': field.help, 'needed': field.needed, - 'readonly': this.options.readonly || - this.getArrayMgr("readonlys").isReadOnly(id), + 'readonly': this.getArrayMgr("readonlys").isReadOnly(id, null, this.options.readonly), 'value': this.options.value[this.prefix+field_name] }; // Can't have a required readonly, it will warn & be removed later, so avoid the warning @@ -297,7 +296,7 @@ var et2_customfields_list = et2_valueWidget.extend([et2_IDetachedDOM, et2_IInput // Set the value for this element var contentMgr = this.getArrayMgr("content"); if (contentMgr != null) { - var val = contentMgr.getEntry(this.id); + var val = contentMgr.getEntry(this.id); _attrs["value"] = {}; if (val !== null) { From a0c38411e992844f899528d6015deabbd40b7163 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Mon, 27 Jul 2015 17:16:11 +0000 Subject: [PATCH 034/115] Make sure link_entry & file_upload are still present before trying to destroy them. Fixes hidden error when calling etemplate2.clear() with read-only link widgets. --- etemplate/js/et2_widget_link.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/etemplate/js/et2_widget_link.js b/etemplate/js/et2_widget_link.js index 3e81e77d59..b25bf5ce71 100644 --- a/etemplate/js/et2_widget_link.js +++ b/etemplate/js/et2_widget_link.js @@ -96,10 +96,16 @@ var et2_link_to = et2_inputWidget.extend( this.link_button = null; this.status_span = null; - this.link_entry.destroy(); - this.link_entry = null; - this.file_upload.destroy(); - this.file_upload = null; + if(this.link_entry) + { + this.link_entry.destroy(); + this.link_entry = null; + } + if(this.file_upload) + { + this.file_upload.destroy(); + this.file_upload = null; + } this.div = null; this._super.apply(this, arguments); From 0c7e1040274bd06fea61fb0a48cd74fa277aa844 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Mon, 27 Jul 2015 17:46:45 +0000 Subject: [PATCH 035/115] More work in progress of mail tree: -Initiate first level of mailboxes in subscription dialog, and let autoloading do the rest of expensive operation -Save last state of tree in subscription dialog -Uses new approach for comparing subscribed and unsubscribed folders --- mail/inc/class.mail_tree.inc.php | 61 ++++++++++++++++---------- mail/inc/class.mail_ui.inc.php | 65 ++++++++++++++++++++-------- mail/js/app.js | 39 ++++++++++++++++- mail/templates/default/subscribe.xet | 4 +- 4 files changed, 126 insertions(+), 43 deletions(-) diff --git a/mail/inc/class.mail_tree.inc.php b/mail/inc/class.mail_tree.inc.php index 1b8f7c945a..b7d43cfdd4 100644 --- a/mail/inc/class.mail_tree.inc.php +++ b/mail/inc/class.mail_tree.inc.php @@ -97,20 +97,36 @@ class mail_tree * @param string $path a node path * @return array returns an array of data extracted from given node path */ - static function getFolderData ($_path, $_hDelimiter) + static function pathToFolderData ($_path, $_hDelimiter) { list(,$path) = explode(self::$delimiter, $_path); - $parts = explode($_hDelimiter, $path); + $path_chain = $parts = explode($_hDelimiter, $path); $name = array_pop($parts); return array ( 'name' => $name, 'mailbox' => $path, 'parent' => implode($_hDelimiter, $parts), 'text' => $name, - 'tooltip' => $name + 'tooltip' => $name, + 'path' => $path_chain ); } + /** + * Check if a given node has children attribute set + * + * @param array $_node array of a node + * @return int returns 1 if it has children flag set otherwise 0 + */ + private static function nodeHasChildren ($_node) + { + $hasChildren = 0; + if (in_array('\haschildren', $_node['ATTRIBUTES']) || + in_array('\Haschildren', $_node['ATTRIBUTES']) || + in_array('\HasChildren', $_node['ATTRIBUTES'])) $hasChildren = 1; + return $hasChildren; + } + /** * getTree provides tree structure regarding to selected node * @@ -118,22 +134,15 @@ class mail_tree * @param string $_profileID = '' icServer id * @param int|boolean $_openTopLevel = 1 Open top level folders on load if it's set to 1|true, * false|0 leaves them in closed state + * @param $_noCheckboxNS = false no checkbox for namesapaces makes sure to not put checkbox for namespaces node * * @return array returns an array of mail tree structure according to provided node */ - function getTree ($_parent = null, $_profileID = '', $_openTopLevel = 1) + function getTree ($_parent = null, $_profileID = '', $_openTopLevel = 1, $_noCheckboxNS = false) { //Init mail folders $tree = array(tree::ID=> $_parent?$_parent:0,tree::CHILDREN => array()); $hDelimiter = $this->ui->mail_bo->getHierarchyDelimiter(); - $fn_nodeHasChildren = function ($_node) - { - $hasChildren = 0; - if (in_array('\haschildren', $_node['ATTRIBUTES']) || - in_array('\Haschildren', $_node['ATTRIBUTES']) || - in_array('\HasChildren', $_node['ATTRIBUTES'])) $hasChildren = 1; - return $hasChildren; - }; if ($_parent) list($_profileID) = explode(self::$delimiter, $_parent); @@ -151,20 +160,20 @@ class mail_tree { try { - $nodeInfo = self::getFolderData($_parent, $hDelimiter); - $folders = $this->ui->mail_bo->getFolderArray($nodeInfo['mailbox']); + $nodeInfo = self::pathToFolderData($_parent, $hDelimiter); + $folders = $this->ui->mail_bo->getFolderArray($nodeInfo['mailbox'],false,2); } catch (Exception $ex) { return self::treeLeafNoConnectionArray($_profileID, $ex->getMessage(),array($_profileID), ''); } - + $childrenNode = array(); foreach ($folders as &$node) { $nodeId = $_profileID.self::$delimiter.$node['MAILBOX']; - $nodeData = self::getFolderData($nodeId, $node['delimiter']); + $nodeData = self::pathToFolderData($nodeId, $node['delimiter']); $childrenNode[] = array( tree::ID=> $nodeId, - tree::AUTOLOAD_CHILDREN => $fn_nodeHasChildren($node), + tree::AUTOLOAD_CHILDREN => self::nodeHasChildren($node), tree::CHILDREN =>array(), tree::LABEL => $nodeData['text'], tree::TOOLTIP => $nodeData['tooltip'], @@ -201,6 +210,7 @@ class mail_tree 'sieve' => $accObj->imapServer()->acc_sieve_enabled, 'spamfolder'=> $accObj->imapServer()->acc_folder_junk?true:false ), + tree::NOCHECKBOX => $_noCheckboxNS ); self::setOutStructure($baseNode, $tree,self::$delimiter); } @@ -212,14 +222,21 @@ class mail_tree foreach ($foldersList as $index => $topFolder) { + $nameSpaces = $this->ui->mail_bo->_getNameSpaces(); + $noCheckbox = false; + foreach ($nameSpaces as &$ns) + { + if($_noCheckboxNS && $ns['prefix'] === $index.$hDelimiter) $noCheckbox = true; + } $parentNode = array( tree::ID=>$_profileID.self::$delimiter.$topFolder[$index]['MAILBOX'], - tree::AUTOLOAD_CHILDREN => $fn_nodeHasChildren($topFolder[$index]), + tree::AUTOLOAD_CHILDREN => self::nodeHasChildren($topFolder[$index]), tree::CHILDREN =>array(), tree::LABEL =>lang($topFolder[$index]['MAILBOX']), tree::OPEN => $_openTopLevel, tree::TOOLTIP => lang($topFolder[$index]['MAILBOX']), - tree::CHECKED => $topFolder[$index]['SUBSCRIBED'] + tree::CHECKED => $topFolder[$index]['SUBSCRIBED'], + tree::NOCHECKBOX => $noCheckbox ); if ($index === "INBOX") { @@ -267,7 +284,7 @@ class mail_tree $childNode = array( tree::ID => $nodeId, - tree::AUTOLOAD_CHILDREN => $fn_nodeHasChildren($node), + tree::AUTOLOAD_CHILDREN => self::nodeHasChildren($node), tree::CHILDREN => array(), tree::LABEL => lang($folderName), 'parent' => $parentPath, @@ -387,5 +404,5 @@ class mail_tree $insert['item'][] = $data; //error_log(__METHOD__."() leaving with out=".array2string($out)); } - -} \ No newline at end of file + +} diff --git a/mail/inc/class.mail_ui.inc.php b/mail/inc/class.mail_ui.inc.php index d03db7002a..322f55b3a7 100644 --- a/mail/inc/class.mail_ui.inc.php +++ b/mail/inc/class.mail_ui.inc.php @@ -238,10 +238,11 @@ class mail_ui /** * Ajax function to request next branch of a tree branch */ - static function ajax_tree_autoloading () + static function ajax_tree_autoloading ($_id = null) { $mail_ui = new mail_ui(); - etemplate_widget_tree::send_quote_json($mail_ui->mail_tree->getTree($_GET['id'])); + $_id = $_id? $_id:$_GET['id']; + etemplate_widget_tree::send_quote_json($mail_ui->mail_tree->getTree($_id,'',1,false)); } /** @@ -262,19 +263,28 @@ class mail_ui { egw_framework::window_close('Missing acc_id!'); } - $sel_options['foldertree'] = $this->getFolderTree(false, $profileId, false, false); - + // Initial tree's options, the rest would be loaded dynamicaly by autoloading, + // triggered from client-side. Also, we keep this here as + $sel_options['foldertree'] = $this->mail_tree->getTree(null,$profileId,1,true); + + //Get all subscribed folder + // as getting all subscribed folder is very fast operation + // we can use it to get a comparison base for folders which + // got subscribed or unsubscribed by the user + try { + $subscribed = $this->mail_bo->icServer->listSubscribedMailboxes('',0,true); + } catch (Exception $ex) { + egw_framework::message($ex->getMessage()); + } + if (!is_array($content)) { $content['foldertree'] = array(); - $allFolders = $this->mail_bo->getFolderObjects(false,false,false,false); - foreach ($allFolders as $folder) + + foreach ($subscribed as $folder) { - $folderName = $profileId . self::$delimiter . $folder->folderName; - if ($folder->subscribed) - { - array_push($content['foldertree'], $folderName); - } + $folderName = $profileId . self::$delimiter . $folder['MAILBOX']; + array_push($content['foldertree'], $folderName); } } else @@ -291,13 +301,30 @@ class mail_ui { $namespace_roots[] = $profileId . self::$delimiter . str_replace($namespace['delimiter'], '', $namespace['prefix']); } - error_log(__METHOD__."() namespace_roots=".array2string($namespace_roots)); - $to_subscribe = array_diff($content['foldertree'], $content['current_subscribed'], $namespace_roots); - $to_unsubscribe = array_diff($content['current_subscribed'], $content['foldertree'], $namespace_roots); + $to_unsubscribe = $to_subscribe = array(); + //$allFoldersData = $this->mail_bo->getFolderArray(null,false,0); + foreach ($content['foldertree'] as $path => $value) + { + list(,$node) = explode($profileId.self::$delimiter, $path); + if ($node) + { + if (is_array($subscribed) && $subscribed[$node] && !$value['value']) $to_unsubscribe []= $node; + if (is_array($subscribed) && !$subscribed[$node] && $value['value']) $to_subscribe [] = $node; + if ($value['value']) $cont[] = $path; + } + + } + $content['foldertree'] = $cont; + // set foldertree options to basic node in order to avoid initial autoloading + // from client side, as no options would trigger that. + $sel_options['foldertree'] = array('id' => '0', 'item'=> array()); foreach(array_merge($to_subscribe, $to_unsubscribe) as $mailbox) { + if (in_array($profileId.self::$delimiter.$mailbox, $namespace_roots, true)) + { + continue; + } $subscribe = in_array($mailbox, $to_subscribe); - list(,$mailbox) = explode(self::$delimiter, $mailbox); // remove profileId and delimiter try { $this->mail_bo->icServer->subscribeMailbox($mailbox, $subscribe); } @@ -329,7 +356,9 @@ class mail_ui // update foldertree in main window $parentFolder='INBOX'; $refreshData = array( - $profileId => lang($parentFolder) + $profileId => lang($parentFolder), + 'subscribed' => $to_subscribe, + 'unsubscribed' => $to_unsubscribe ); $response = egw_json_response::get(); foreach($refreshData as $folder => &$name) @@ -337,7 +366,9 @@ class mail_ui $name = $this->getFolderTree(true, $folder, true, true, false); } // give success/error message to opener and popup itself + //$response->call('opener.app.mail.subscription_refresh',$refreshData); $response->call('opener.app.mail.mail_reloadNode',$refreshData); + egw_framework::refresh_opener($msg, 'mail', null, null, null, null, null, $msg_type); if ($button == 'apply') { @@ -353,7 +384,7 @@ class mail_ui } $preserv['profileId'] = $profileId; - $preserv['current_subscribed'] = $content['foldertree']; + $readonlys = array(); $stmpl->exec('mail.mail_ui.subscription', $content,$sel_options,$readonlys,$preserv,2); diff --git a/mail/js/app.js b/mail/js/app.js index 12ad6597a2..8f19df9c03 100644 --- a/mail/js/app.js +++ b/mail/js/app.js @@ -44,7 +44,11 @@ app.classes.mail = AppJS.extend( timeout: null, request: null }, - + /** + * + */ + subscription_treeLastState : "", + /** * abbrevations for common access rights * @array @@ -277,6 +281,15 @@ app.classes.mail = AppJS.extend( jQuery('input',to.node).focus(); } break; + case 'mail.subscribe': + if (this.subscription_treeLastState != "") + { + var tree = this.et2.getWidgetById('foldertree'); + //Saved state of tree + var state = jQuery.parseJSON(this.subscription_treeLastState); + + tree.input.loadJSONObject(tree._htmlencode_node(state)); + } } }, @@ -3654,7 +3667,29 @@ app.classes.mail = AppJS.extend( var acc_id = parseInt(_senders[0].id); this.egw.open_link('mail.mail_sieve.editVacation&acc_id='+acc_id,'_blank','700x480'); }, - + + subscription_refresh: function(_data) + { + console.log(_data); + }, + + /** + * Submit on apply button and save current tree state + * + * @param {type} _egw + * @param {type} _widget + * @returns {undefined} + */ + subscription_apply: function (_egw, _widget) + { + var tree = etemplate2.getByApplication('mail')[0].widgetContainer.getWidgetById('foldertree'); + if (tree) + { + tree.input._xfullXML = true; + this.subscription_treeLastState = tree.input.serializeTreeToJSON(); + } + this.et2._inst.submit(_widget); + }, /** * Popup the subscription dialog * diff --git a/mail/templates/default/subscribe.xet b/mail/templates/default/subscribe.xet index 2b0107e6cc..8495bd8669 100755 --- a/mail/templates/default/subscribe.xet +++ b/mail/templates/default/subscribe.xet @@ -13,14 +13,14 @@ - +