diff --git a/api/src/Db.php b/api/src/Db.php index 1f15ef54ab..af9c1bad2a 100644 --- a/api/src/Db.php +++ b/api/src/Db.php @@ -2287,7 +2287,7 @@ class Db if ($this->Debug) echo "

sql='$sql'

"; - if ($line === false && $file === false) // call by union, to return the sql rather then run the query + if ($line === false && $file === false) // call by union, to return the sql rather than run the query { return $sql; } diff --git a/api/src/Storage/Tracking.php b/api/src/Storage/Tracking.php index 3a88b50d2d..47d3bf435d 100644 --- a/api/src/Storage/Tracking.php +++ b/api/src/Storage/Tracking.php @@ -596,7 +596,7 @@ abstract class Tracking if (!$this->notify_current_user && $this->user) // do we have a current user and should we notify the current user about his own changes { - //error_log("do_notificaton() adding user=$this->user to email_sent, to not notify him"); + //error_log("do_notification() adding user=$this->user to email_sent, to not notify him"); $email_sent[] = $GLOBALS['egw']->accounts->id2name($this->user,'account_email'); } $skip_notify = $this->get_config('skip_notify',$data,$old); @@ -722,8 +722,8 @@ abstract class Tracking * * Called by track() or externally for sending async notifications * - * Method changes $GLOBALS['egw_info']['user'], so everything called by it, eg. get_(subject|body|links|attachements), - * must NOT store something from user enviroment! By the end of the method, everything get changed back. + * Method changes $GLOBALS['egw_info']['user'], so everything called by it, e.g. get_(subject|body|links|attachments), + * must NOT store something from user environment! By the end of the method, everything get changed back. * * @param array $data current entry * @param array $old = null old/last state of the entry or null for a new entry @@ -807,7 +807,7 @@ abstract class Tracking $attachments = $this->get_attachments($data,$old,$receiver); } - // restore user enviroment BEFORE calling notification class or returning + // restore user environment BEFORE calling notification class or returning $GLOBALS['egw_info']['user'] = $save_user; // need to call preferences constructor and read_repository, to set user timezone again $GLOBALS['egw']->preferences->__construct($GLOBALS['egw_info']['user']['account_id']); @@ -843,12 +843,12 @@ abstract class Tracking $notification->set_reply_to($reply_to); $notification->set_subject($subject); $notification->set_links(array($link)); - $notification->set_popupdata($link?$link['app']:null, $link); + $notification->set_popupdata($link['app']??null, $link, $link['id']??null); if ($attachments && is_array($attachments)) { $notification->set_attachments($attachments); } - // run immediatly during async service, as sending mail with Horde fails, if PHP is already in shutdown + // run immediately during async service, as sending mail with Horde fails, if PHP is already in shutdown // (json requests take care of that by calling Egw::__desctruct() explicit before it's regular triggered) $run = isset($GLOBALS['egw_info']['flags']['async-service']) ? 'call_user_func_array' : Api\Egw::class.'::on_shutdown'; $run(static function($notification, $sender, $receiver, $subject) @@ -1088,7 +1088,7 @@ abstract class Tracking * @param array $data * @param array $old * @param boolean $integrate_link to have links embedded inside the body - * @param int|string $receiver nummeric account_id or email address + * @param int|string $receiver numeric account_id or email address * @return string */ public function get_body($html_email,$data,$old,$integrate_link = true,$receiver=null) diff --git a/calendar/inc/class.calendar_boupdate.inc.php b/calendar/inc/class.calendar_boupdate.inc.php index f0b8b69369..2640dd4cdf 100644 --- a/calendar/inc/class.calendar_boupdate.inc.php +++ b/calendar/inc/class.calendar_boupdate.inc.php @@ -1236,9 +1236,9 @@ class calendar_boupdate extends calendar_bo // popup notifiactions: set subject, different message (without separator) and (always) links $notification->set_popupsubject($subject); - if ($method =='REQUEST') + if ($method == 'REQUEST') { - // Add ACCEPT|REHECT|TENTATIVE actions + // Add ACCEPT|REJECT|TENTATIVE actions $notification->set_popupdata('calendar', array( 'event_id' => $event['id'], 'user_id' => $userid, diff --git a/notifications/inc/class.notifications.inc.php b/notifications/inc/class.notifications.inc.php index bd031dad63..6f035650d9 100644 --- a/notifications/inc/class.notifications.inc.php +++ b/notifications/inc/class.notifications.inc.php @@ -593,7 +593,7 @@ class notifications { } break; // stop running through chain } - // backend sucseeded + // backend succeeded $user_notified = true; if($action == 'stop' || $action == 'fail') { break; } // stop running through chain } @@ -827,13 +827,15 @@ class notifications { * Set popup data * * @param string $_appname - * @param array $_data + * @param ?array $_data + * * @return boolean */ - public function set_popupdata($_appname, $_data) + public function set_popupdata(string $_appname, ?array $_data, ?int $_id=null) { $this->popup_data = array( 'appname' => $_appname, + 'id' => $_id ?: $_data['id'] ?? null, 'data' => $_data ); diff --git a/notifications/inc/class.notifications_ajax.inc.php b/notifications/inc/class.notifications_ajax.inc.php index cd3da43325..1b1bf4574d 100644 --- a/notifications/inc/class.notifications_ajax.inc.php +++ b/notifications/inc/class.notifications_ajax.inc.php @@ -129,13 +129,14 @@ class notifications_ajax } /** - * Remove given notification id(s) from the table + * Remove given notification id(s) and app_ids from the table * * @param int[]|array[] $notifymessages one or multiple notify_id(s) or objects incl. id attribute + * @param array[] $app_ids app-name int[] pairs */ - public function delete_message(array $notifymessages) + public function delete_message(array $notifymessages, array $app_ids=[]) { - $this->update($notifymessages, null); // null = delete + $this->update($notifymessages, null, $app_ids ?? []); // null = delete // if we delete all messages (we either delete one or all!), we return the next chunk of messages directly if (count($notifymessages) > 1) @@ -165,11 +166,12 @@ class notifications_ajax * * @param array $notifymessages * @param string|null $status use null to delete + * @param array[] $app_ids app-name int[] pairs * @return array */ - protected function update(array $notifymessages, $status='SEEN') + protected function update(array $notifymessages, $status='SEEN', array $app_ids=[]) { - $notify_ids = $app_ids = []; + $notify_ids = []; foreach ($notifymessages as $data) { if (is_array($data) && !empty($data['id'])) @@ -187,14 +189,13 @@ class notifications_ajax } $cut_off = $this->db->quote(Api\DateTime::to(self::CUT_OFF_DATE, Api\DateTime::DATABASE)); try { - // MariaDB code using JSON_EXTRACT() foreach($app_ids as $app => $ids) { $where = [ 'account_id' => $this->recipient->account_id, 'notify_type' => self::_type, - "JSON_EXTRACT(notify_data, '$.appname') = ".$this->db->quote($app), - "JSON_EXTRACT(notify_data, '$.data.id') IN (".implode(',', array_map([$this->db, 'quote'], array_unique($ids))).')', + 'notify_app' => $app, + 'notify_app_id' => array_unique($ids), 'notify_created > '.$cut_off, ]; if (isset($status)) @@ -207,21 +208,8 @@ class notifications_ajax } } } - // other DBs catch (Api\Db\Exception $e) { - foreach($this->db->select(self::_notification_table, 'notify_id,notify_data', [ - 'account_id' => $this->recipient->account_id, - 'notify_type' => self::_type, - 'notify_created > '.$cut_off, - "notify_data <> '[]'", // does not return NULL or '[]' rows - ], __LINE__, __FILE__, false,'', self::_appname) as $row) - { - if (($data = json_decode($row['notify_data'], true)) && - isset($data['data']['id']) && in_array($data['data']['id'], $app_ids[$data['appname']] ?? [])) - { - $notify_ids[] = $row['notify_id']; - } - } + // ignore, if DB is not yet updated with notify_app(_id) columns } $where = [ 'notify_id' => array_unique($notify_ids), diff --git a/notifications/inc/class.notifications_popup.inc.php b/notifications/inc/class.notifications_popup.inc.php index 45189ca7c0..24b28fa51d 100644 --- a/notifications/inc/class.notifications_popup.inc.php +++ b/notifications/inc/class.notifications_popup.inc.php @@ -18,7 +18,7 @@ use EGroupware\Api; * @abstract egwpopup is a two stage notification. In the first stage * notification is written into self::_notification_table. * In the second stage a request from the client reads - * out the table to look if there is a notificaton for this + * out the table to look if there is a notification for this * client. The second stage is done in class.notifications_ajax.inc.php */ class notifications_popup implements notifications_iface @@ -128,6 +128,8 @@ class notifications_popup implements notifications_iface 'notify_type' => self::_type, 'notify_data' => $_data && is_array($_data) ? json_encode($_data) : NULL, 'notify_created' => new Api\DateTime(), + 'notify_app' => $_data['appname'], + 'notify_app_id' => $_data['id'], ), false,__LINE__,__FILE__,self::_appname); if ($result === false) throw new Exception("Can't save notification into SQL table"); $push = new Api\Json\Push($this->recipient->account_id); @@ -154,10 +156,20 @@ class notifications_popup implements notifications_iface $result = []; if (($total = $db->select(self::_notification_table, 'COUNT(*)', [ - 'account_id' => $_account_id, - 'notify_type' => self::_type, - 'notify_created > '.($cut_off=$db->quote(Api\DateTime::to(notifications_ajax::CUT_OFF_DATE, Api\DateTime::DATABASE))), - ], __LINE__, __FILE__, false, '', self::_appname)->fetchColumn())) + 'account_id' => $_account_id, + 'notify_type' => self::_type, + 'notify_created > '.($cut_off=$db->quote(Api\DateTime::to(notifications_ajax::CUT_OFF_DATE, Api\DateTime::DATABASE))), + 'notify_app_id IS NULL', + ], __LINE__, __FILE__, false, '', self::_appname)->fetchColumn()+ + $db->select( + '('.$db->select(self::_notification_table, 'notify_app,notify_app_id', [ + 'account_id' => $_account_id, + 'notify_type' => self::_type, + 'notify_created > '.($cut_off=$db->quote(Api\DateTime::to(notifications_ajax::CUT_OFF_DATE, Api\DateTime::DATABASE))), + 'notify_app_id IS NOT NULL', + ], false, false, false, 'GROUP BY notify_app,notify_app_id', self::_appname).') AS app_ids', + 'COUNT(*)', false, __LINE__, __FILE__, false, '', self::_appname)->fetchColumn() + )) { $n = 0; $chunk_size = 150; @@ -188,26 +200,22 @@ class notifications_popup implements notifications_iface 'current' => new Api\DateTime('now'), 'actions' => is_array($actions) ? $actions : NULL, 'extra_data' => $data['data'] ?? [], + 'app' => $notification['notify_app'] ?? $data['data']['app'] ?? null, + 'app_id' => $notification['notify_app_id'] ?? $data['data']['id'] ?? null, ]; // aggregate by app:id reporting only the newest entry - if (!empty($data['extra_data']['id'])) + if (!empty($data['app_id'])) { - if (!isset($result[$id = $data['extra_data']['app'] . ':' . $data['extra_data']['id']])) + if (!isset($result[$id = $data['app'] . ':' . $data['app_id']])) { $result[$id] = $data; } - else - { - $total--; - /* in case we want to show all - $result['id']['others'][] = $data; - */ - } } else { $result[] = $data; } + if (count($result) >= min($num_rows, $total)) break; } $n += $chunk_size; } diff --git a/notifications/js/notificationajaxpopup.js b/notifications/js/notificationajaxpopup.js index 4344810388..2227c7f840 100644 --- a/notifications/js/notificationajaxpopup.js +++ b/notifications/js/notificationajaxpopup.js @@ -6,7 +6,6 @@ * @subpackage ajaxpoup * @link http://www.egroupware.org * @author Cornelius Weiss , Christian Binder , Ralf Becker - * @version $Id$ */ 'use strict'; @@ -570,7 +569,17 @@ notifications.prototype.delete_all = function () { if (!notifymessages || Object.entries(notifymessages).length == 0) return false; - egw.request("notifications.notifications_ajax.delete_message", [Object.keys(notifymessages)]); + const app_ids = {}; + for(const id in notifymessages) + { + const notification = notifymessages[id]; + if (notification.data?.id && notification.data.app) + { + if (typeof app_ids[notification.data.app] === "undefined") app_ids[notification.data.app] = []; + app_ids[notification.data.app].push(notification.data.id); + } + } + egw.request("notifications.notifications_ajax.delete_message", [Object.keys(notifymessages), app_ids]); notifymessages = {}; _currentRawData = []; // otherwise response from delete_message/get_notifications might not get parsed this.total = 0; @@ -585,11 +594,14 @@ */ notifications.prototype.button_delete = function(_node, _event) { _event.stopPropagation(); - var egwpopup_message = _node[0]; - var id = egwpopup_message[0].id.replace(/egwpopup_message_/ig,''); - egw.request("notifications.notifications_ajax.delete_message", [[id]]); - var nextNode = egwpopup_message.next(); - var keepLoadingPrompt = false; + const egwpopup_message = _node[0]; + const id = egwpopup_message[0].id.replace(/egwpopup_message_/ig,''); + const notification = notifymessages[id]; + const app_ids = {}; + if (notification.data?.id) app_ids[notification.data.app] = [notification.data.id]; + egw.request("notifications.notifications_ajax.delete_message", [[id], app_ids]); + const nextNode = egwpopup_message.next(); + let keepLoadingPrompt = false; delete (notifymessages[id]); this.total -= 1; this.counterUpdate(); @@ -935,7 +947,8 @@ // toggle notifications bar jQuery('.button_right_toggle', '#egwpopup').click(function(){window.app.notifications.toggle();}); $egwpopup_fw.click(function(){window.app.notifications.toggle();}); - jQuery(".egwpopup_deleteall", '#egwpopup').click(function(){ + jQuery(".egwpopup_deleteall", '#egwpopup').click(function(_ev){ + _ev.stopPropagation(); et2_dialog.show_dialog( function(_button){ if (_button == 2) window.app.notifications.delete_all(); }, diff --git a/notifications/setup/setup.inc.php b/notifications/setup/setup.inc.php index 460753305c..4f77babce9 100644 --- a/notifications/setup/setup.inc.php +++ b/notifications/setup/setup.inc.php @@ -14,7 +14,7 @@ if (!defined('NOTIFICATION_APP')) } $setup_info[NOTIFICATION_APP]['name'] = NOTIFICATION_APP; -$setup_info[NOTIFICATION_APP]['version'] = '23.1'; +$setup_info[NOTIFICATION_APP]['version'] = '23.1.001'; $setup_info[NOTIFICATION_APP]['app_order'] = 1; $setup_info[NOTIFICATION_APP]['tables'] = array('egw_notificationpopup'); $setup_info[NOTIFICATION_APP]['enable'] = 2; @@ -39,4 +39,4 @@ $setup_info[NOTIFICATION_APP]['hooks']['config'] = 'notifications.notifications. $setup_info[NOTIFICATION_APP]['depends'][] = array( 'appname' => 'api', 'versions' => Array('23.1') -); \ No newline at end of file +); diff --git a/notifications/setup/tables_current.inc.php b/notifications/setup/tables_current.inc.php index 610bdfe7cd..c3e3d971e4 100644 --- a/notifications/setup/tables_current.inc.php +++ b/notifications/setup/tables_current.inc.php @@ -18,11 +18,13 @@ $phpgw_baseline = array( 'notify_created' => array('type' => 'timestamp','meta' => 'timestamp','default' => 'current_timestamp','comment' => 'creation time of notification'), 'notify_type' => array('type' => 'ascii','precision' => '32','comment' => 'notification type'), 'notify_status' => array('type' => 'varchar','precision' => '32','comment' => 'notification status'), - 'notify_data' => array('type' => 'varchar','precision' => '4096','comment' => 'notification data') + 'notify_data' => array('type' => 'varchar','precision' => '4096','comment' => 'notification data'), + 'notify_app' => array('type' => 'ascii','precision' => '16','comment' => 'appname'), + 'notify_app_id' => array('type' => 'ascii','precision' => '64','comment' => 'application id') ), 'pk' => array('notify_id'), 'fk' => array(), - 'ix' => array('account_id','notify_created'), + 'ix' => array('notify_created',array('account_id','notify_type'),array('notify_app','notify_app_id')), 'uc' => array() ) -); +); \ No newline at end of file diff --git a/notifications/setup/tables_update.inc.php b/notifications/setup/tables_update.inc.php index ccf1ff69dc..7689a328b9 100644 --- a/notifications/setup/tables_update.inc.php +++ b/notifications/setup/tables_update.inc.php @@ -9,6 +9,8 @@ * @subpackage setup */ +use EGroupware\Api; + function notifications_upgrade0_5() { $GLOBALS['egw_setup']->oProc->AlterColumn('egw_notificationpopup','account_id',array( @@ -201,4 +203,60 @@ function notifications_upgrade21_1() )); return $GLOBALS['setup_info']['notifications']['currentver'] = '23.1'; +} + +/** + * Add explicit columns for app-name and -id + * + * @return string + */ +function notifications_upgrade23_1() +{ + $GLOBALS['egw_setup']->oProc->AddColumn('egw_notificationpopup','notify_app',array( + 'type' => 'ascii', + 'precision' => '16', + 'comment' => 'appname' + )); + + $GLOBALS['egw_setup']->oProc->AddColumn('egw_notificationpopup','notify_app_id',array( + 'type' => 'ascii', + 'precision' => '64', + 'comment' => 'application id' + )); + + /** @var Api\Db $db */ + $db = $GLOBALS['egw_setup']->db; + try { + $db->update('egw_notificationpopup',array( + "notify_app=JSON_VALUE(notify_data,'$.data.app')", + "notify_app_id=JSON_VALUE(notify_data,'$.data.id')", + ), 'notify_data IS NOT NULL', __LINE__, __FILE__, 'notifications'); + } + catch (Api\Db\Exception\InvalidSql $e) { + $start = 0; + $chunk_size = 1000; + do + { + foreach($rs=$db->select('egw_notificationpopup','notify_id,notify_data','notify_data IS NOT NULL', + __LINE__, __FILE__, $start, '', 'notifications', $chunk_size) as $row) + { + $data = json_decode($row['notify_data']); + if ($data && !empty($data['data']['id'])) + { + $db->update('egw_notficationpopup', [ + 'notify_app' => $data['data']['app'], + 'notify_id' => $data['data']['id'], + ], ['notify_id' => $row['notify_id']], __LINE__, __FILE__, 'notifications'); + } + ++$start; + } + } + while ($start && !($start % $chunk_size)); + } + + $GLOBALS['egw_setup']->oProc->CreateIndex('egw_notificationpopup', array('notify_app','notify_app_id'), false); + $GLOBALS['egw_setup']->oProc->CreateIndex('egw_notificationpopup', array('account_id','notify_type'), false); + $GLOBALS['egw_setup']->oProc->DropIndex('egw_notificationpopup', array('account_id')); + + return $GLOBALS['setup_info']['notifications']['currentver'] = '23.1.001'; } \ No newline at end of file