From 7f5a849e7816e7b4c7b13a72d38a9c777632d7cd Mon Sep 17 00:00:00 2001
From: alecpl <alec@alec.pl>
Date: Thu, 07 Jul 2011 07:44:26 -0400
Subject: [PATCH] - Added possibility to undo last contact delete operation

---
 skins/default/common.css                |    6 +
 CHANGELOG                               |    1 
 program/steps/addressbook/undo.inc      |   87 +++++++++++++++++++++
 program/include/rcube_json_output.php   |    5 
 program/steps/addressbook/func.inc      |    6 +
 program/steps/addressbook/delete.inc    |   22 +++++
 skins/default/templates/mail.html       |    2 
 program/include/rcube_addressbook.php   |   13 +++
 program/include/rcmail.php              |    2 
 program/localization/en_US/messages.inc |    3 
 program/localization/en_US/labels.inc   |    1 
 program/localization/pl_PL/labels.inc   |    1 
 program/include/rcube_template.php      |   13 +-
 program/localization/pl_PL/messages.inc |    3 
 program/js/app.js                       |   26 ++++--
 program/include/rcube_contacts.php      |   34 ++++++++
 16 files changed, 201 insertions(+), 24 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 27de618..8748ecc 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,7 @@
 CHANGELOG Roundcube Webmail
 ===========================
 
+- Added possibility to undo last contact delete operation
 - Fix sorting of contact groups after group create (#1487747)
 - Add optional textual upload progress indicator (#1486039)
 - Fix parsing URLs containing commas (#1487970)
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index e94e205..fe2d994 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -507,7 +507,7 @@
     $this->output->set_env('comm_path', $this->comm_path);
     $this->output->set_charset(RCMAIL_CHARSET);
 
-    // add some basic label to client
+    // add some basic labels to client
     $this->output->add_label('loading', 'servererror');
 
     return $this->output;
diff --git a/program/include/rcube_addressbook.php b/program/include/rcube_addressbook.php
index cefe461..3581b83 100644
--- a/program/include/rcube_addressbook.php
+++ b/program/include/rcube_addressbook.php
@@ -38,6 +38,7 @@
     public $primary_key;
     public $groups = false;
     public $readonly = true;
+    public $undelete = false;
     public $ready = false;
     public $group_id = null;
     public $list_page = 1;
@@ -256,7 +257,17 @@
     }
 
     /**
-     * Remove all records from the database
+     * Unmark delete flag on contact record(s)
+     *
+     * @param array  Record identifiers
+     */
+    function undelete($ids)
+    {
+        /* empty for read-only address books */
+    }
+
+    /**
+     * Mark all records in database as deleted
      */
     function delete_all()
     {
diff --git a/program/include/rcube_contacts.php b/program/include/rcube_contacts.php
index b097b3b..52667fa 100644
--- a/program/include/rcube_contacts.php
+++ b/program/include/rcube_contacts.php
@@ -52,6 +52,7 @@
     public $primary_key = 'contact_id';
     public $readonly = false;
     public $groups = true;
+    public $undelete = true;
     public $list_page = 1;
     public $page_size = 10;
     public $group_id = 0;
@@ -692,12 +693,43 @@
 
 
     /**
+     * Undelete one or more contact records
+     *
+     * @param array  Record identifiers
+     */
+    function undelete($ids)
+    {
+        if (!is_array($ids))
+            $ids = explode(',', $ids);
+
+        $ids = $this->db->array2list($ids, 'integer');
+
+        // flag record as deleted
+        $this->db->query(
+            "UPDATE ".get_table_name($this->db_name).
+            " SET del=0, changed=".$this->db->now().
+            " WHERE user_id=?".
+                " AND contact_id IN ($ids)",
+            $this->user_id
+        );
+
+        $this->cache = null;
+
+        return $this->db->affected_rows();
+    }
+
+
+    /**
      * Remove all records from the database
      */
     function delete_all()
     {
-        $this->db->query("DELETE FROM ".get_table_name($this->db_name)." WHERE user_id = ?", $this->user_id);
         $this->cache = null;
+
+        $this->db->query("UPDATE ".get_table_name($this->db_name).
+            " SET del=1, changed=".$this->db->now().
+            " WHERE user_id = ?", $this->user_id);
+
         return $this->db->affected_rows();
     }
 
diff --git a/program/include/rcube_json_output.php b/program/include/rcube_json_output.php
index d1c9de2..efb6728 100644
--- a/program/include/rcube_json_output.php
+++ b/program/include/rcube_json_output.php
@@ -164,14 +164,15 @@
      * @param string  $type     Message type [notice|confirm|error]
      * @param array   $vars     Key-value pairs to be replaced in localized text
      * @param boolean $override Override last set message
+     * @param int     $timeout  Message displaying time in seconds
      * @uses self::command()
      */
-    public function show_message($message, $type='notice', $vars=null, $override=true)
+    public function show_message($message, $type='notice', $vars=null, $override=true, $timeout=0)
     {
         if ($override || !$this->message) {
             $this->message = $message;
             $msgtext = rcube_label_exists($message) ? rcube_label(array('name' => $message, 'vars' => $vars)) : $message;
-            $this->command('display_message', $msgtext, $type);
+            $this->command('display_message', $msgtext, $type, $timeout * 1000);
         }
     }
 
diff --git a/program/include/rcube_template.php b/program/include/rcube_template.php
index 39b2282..a4c1a69 100755
--- a/program/include/rcube_template.php
+++ b/program/include/rcube_template.php
@@ -238,18 +238,19 @@
     /**
      * Invoke display_message command
      *
-     * @param string Message to display
-     * @param string Message type [notice|confirm|error]
-     * @param array Key-value pairs to be replaced in localized text
-     * @param boolean Override last set message
+     * @param string  $message  Message to display
+     * @param string  $type     Message type [notice|confirm|error]
+     * @param array   $vars     Key-value pairs to be replaced in localized text
+     * @param boolean $override Override last set message
+     * @param int     $timeout  Message display time in seconds
      * @uses self::command()
      */
-    public function show_message($message, $type='notice', $vars=null, $override=true)
+    public function show_message($message, $type='notice', $vars=null, $override=true, $timeout=0)
     {
         if ($override || !$this->message) {
             $this->message = $message;
             $msgtext = rcube_label_exists($message) ? rcube_label(array('name' => $message, 'vars' => $vars)) : $message;
-            $this->command('display_message', $msgtext, $type);
+            $this->command('display_message', $msgtext, $type, $timeout * 1000);
         }
     }
 
diff --git a/program/js/app.js b/program/js/app.js
index 6a35c59..125b645 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -164,7 +164,7 @@
     }
 
     // enable general commands
-    this.enable_command('logout', 'mail', 'addressbook', 'settings', 'save-pref', true);
+    this.enable_command('logout', 'mail', 'addressbook', 'settings', 'save-pref', 'undo', true);
 
     if (this.env.permaurl)
       this.enable_command('permaurl', true);
@@ -411,7 +411,7 @@
 
     // show message
     if (this.pending_message)
-      this.display_message(this.pending_message[0], this.pending_message[1]);
+      this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]);
 
     // map implicit containers
     if (this.gui_objects.folderlist)
@@ -1046,6 +1046,10 @@
         this.goto_url('settings/' + command);
         break;
 
+      case 'undo':
+        this.http_request('undo', '', this.display_message('', 'loading'));
+        break;
+
       // unified command call (command name == function name)
       default:
         var func = command.replace(/-/g, '_');
@@ -1296,7 +1300,7 @@
       var toffset = -moffset-boffset;
       var li, div, pos, mouse, check, oldclass,
         layerclass = 'draglayernormal';
-      
+
       if (this.contact_list && this.contact_list.draglayer)
         oldclass = this.contact_list.draglayer.attr('class');
 
@@ -4980,7 +4984,7 @@
     if (elem._placeholder && (!$elem.val() || $elem.val() == elem._placeholder))
       $elem.addClass('placeholder').attr('spellcheck', false).val(elem._placeholder);
   };
-  
+
   // write to the document/window title
   this.set_pagetitle = function(title)
   {
@@ -4989,27 +4993,29 @@
   };
 
   // display a system message, list of types in common.css (below #message definition)
-  this.display_message = function(msg, type)
+  this.display_message = function(msg, type, timeout)
   {
     // pass command to parent window
     if (this.is_framed())
-      return parent.rcmail.display_message(msg, type);
+      return parent.rcmail.display_message(msg, type, timeout);
 
     if (!this.gui_objects.message) {
       // save message in order to display after page loaded
       if (type != 'loading')
-        this.pending_message = new Array(msg, type);
+        this.pending_message = new Array(msg, type, timeout);
       return false;
     }
 
     type = type ? type : 'notice';
 
     var ref = this,
-      key = msg,
+      key = String(msg).replace(this.identifier_expr, '_'),
       date = new Date(),
-      id = type + date.getTime(),
+      id = type + date.getTime();
+
+    if (!timeout)
       timeout = this.message_time * (type == 'error' || type == 'warning' ? 2 : 1);
-      
+
     if (type == 'loading') {
       key = 'loading';
       timeout = this.env.request_timeout * 1000;
diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index b2f136b..60676aa 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -448,6 +448,7 @@
 $labels['sortby'] = 'Sort by';
 $labels['sortasc']  = 'Sort ascending';
 $labels['sortdesc'] = 'Sort descending';
+$labels['undo'] = 'Undo';
 
 // units
 $labels['B'] = 'B';
diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc
index da4b413..e64b229 100644
--- a/program/localization/en_US/messages.inc
+++ b/program/localization/en_US/messages.inc
@@ -128,6 +128,8 @@
 $messages['internalerror'] = 'An internal error occured. Please try again';
 $messages['contactdelerror'] = 'Could not delete contact(s)';
 $messages['contactdeleted'] = 'Contact(s) deleted successfully';
+$messages['contactrestoreerror'] = 'Could not restore deleted contact(s)';
+$messages['contactrestored'] = 'Contact(s) restored successfully';
 $messages['groupdeleted'] = 'Group deleted successfully';
 $messages['grouprenamed'] = 'Group renamed successfully';
 $messages['groupcreated'] = 'Group created successfully';
@@ -142,5 +144,6 @@
 $messages['foldercreated'] = 'Folder created successfully';
 $messages['invalidimageformat'] = 'Not a valid image format';
 $messages['mispellingsfound'] = 'Spelling errors detected in the message';
+$messages['itemsdeleted'] = '$num item(s) has been deleted.';
 
 ?>
diff --git a/program/localization/pl_PL/labels.inc b/program/localization/pl_PL/labels.inc
index adb33e9..aa7127d 100644
--- a/program/localization/pl_PL/labels.inc
+++ b/program/localization/pl_PL/labels.inc
@@ -416,5 +416,6 @@
 $labels['importtarget'] = 'Dodaj nowe kontakty do książki adresowej:';
 $labels['grouprename'] = 'Zmień nazwę grupy';
 $labels['groupdelete'] = 'Usuń grupę';
+$labels['undo'] = 'Cofnij';
 
 ?>
diff --git a/program/localization/pl_PL/messages.inc b/program/localization/pl_PL/messages.inc
index 9c7c22a..6a05244 100644
--- a/program/localization/pl_PL/messages.inc
+++ b/program/localization/pl_PL/messages.inc
@@ -146,5 +146,8 @@
 $messages['importconfirmskipped'] = '<b>Pominięto $skipped istniejących wpisów</b>';
 $messages['invalidimageformat'] = 'Niepoprawny format obrazka';
 $messages['mispellingsfound'] = 'Wykryto błędy pisowni w tej wiadomości';
+$messages['itemsdeleted'] = '$num elemenów zostało usuniętych.';
+$messages['contactrestoreerror'] = 'Przywracanie kontaktów nie powiodło się';
+$messages['contactrestored'] = 'Kontakt(y) zostały przywrócone';
 
 ?>
diff --git a/program/steps/addressbook/delete.inc b/program/steps/addressbook/delete.inc
index af9bdb1..f11752b 100644
--- a/program/steps/addressbook/delete.inc
+++ b/program/steps/addressbook/delete.inc
@@ -26,6 +26,9 @@
 $cids   = rcmail_get_cids();
 $delcnt = 0;
 
+// remove previous deletes
+$RCMAIL->session->remove('contact_undo');
+
 foreach ($cids as $source => $cid)
 {
     $CONTACTS = rcmail_contact_source($source);
@@ -36,6 +39,7 @@
         if (count($cids) == 1) {
             $OUTPUT->show_message('contactdelerror', 'error');
             $OUTPUT->command('list_contacts');
+            $OUTPUT->send();
         }
         continue;
     }
@@ -52,10 +56,13 @@
     }
     else {
         $delcnt += $deleted;
+
+        // store deleted contacts IDs in session for undelete
+        if ($CONTACTS->undelete) {
+            $_SESSION['contact_undo']['data'][$source] = $cid;
+        }
     }
 }
-
-$OUTPUT->show_message('contactdeleted', 'confirmation');
 
 $page = isset($_SESSION['page']) ? $_SESSION['page'] : 1;
 
@@ -135,6 +142,17 @@
 $OUTPUT->set_env('pagecount', ceil($result->count / $CONFIG['pagesize']));
 $OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result));
 
+if (!empty($_SESSION['contact_undo'])) {
+    $_SESSION['contact_undo']['ts'] = time();
+    $msg = html::span(null, rcube_label(array('name' => 'itemsdeleted', 'vars' => array('num' => $deleted))))
+        . ' ' . html::a(array('onclick' => JS_OBJECT_NAME.".command('undo', '', this)"), rcube_label('undo'));
+
+    $OUTPUT->show_message($msg, 'confirmation', null, true, $RCMAIL->config->get('undo_timeout', 15));
+}
+else {
+    $OUTPUT->show_message('contactdeleted', 'confirmation');
+}
+
 // add new rows from next page (if any)
 if (!empty($records)) {
     rcmail_js_contacts_list($records);
diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc
index 4c930c8..a895b61 100644
--- a/program/steps/addressbook/func.inc
+++ b/program/steps/addressbook/func.inc
@@ -88,6 +88,12 @@
     $CONTACTS = rcmail_contact_source($source, true);
 }
 
+// remove undo information...
+if ($undo = $_SESSION['contact_undo']) {
+    // ...after 30 seconds
+    if ($undo['ts'] < time() - 30)
+        $RCMAIL->session->remove('contact_undo');
+}
 
 // instantiate a contacts object according to the given source
 function rcmail_contact_source($source=null, $init_env=false)
diff --git a/program/steps/addressbook/undo.inc b/program/steps/addressbook/undo.inc
new file mode 100644
index 0000000..936f110
--- /dev/null
+++ b/program/steps/addressbook/undo.inc
@@ -0,0 +1,87 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/steps/addressbook/undo.inc                                    |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2011, Kolab Systems AG                                  |
+ | Licensed under the GNU GPL                                            |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Undelete contacts (CIDs) from last delete action                    |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com>                   |
+ +-----------------------------------------------------------------------+
+
+ $Id$
+
+*/
+
+// process ajax requests only
+if (!$OUTPUT->ajax_call)
+    return;
+
+$undo   = $_SESSION['contact_undo'];
+$delcnt = 0;
+
+foreach ((array)$undo['data'] as $source => $cid)
+{
+    $CONTACTS = rcmail_contact_source($source);
+
+    $plugin = $RCMAIL->plugins->exec_hook('contact_undelete', array(
+        'id' => $cid, 'source' => $source));
+
+    $restored = !$plugin['abort'] ? $CONTACTS->undelete($cid) : $plugin['result'];
+
+    if (!$restored) {
+        $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : 'contactrestoreerror', 'error');
+        $OUTPUT->command('list_contacts');
+        $OUTPUT->send();
+    }
+    else {
+        $delcnt += $restored;
+    }
+}
+
+// update saved search after data changed
+if ($delcnt && ($search_request = $_REQUEST['_search']) && isset($_SESSION['search'][$search_request])) {
+    $search  = (array)$_SESSION['search'][$search_request];
+
+    foreach ($search as $s => $set) {
+        $source = $RCMAIL->get_address_book($s);
+
+        // reset page
+        $source->set_page(1);
+        $source->set_pagesize(9999);
+        $source->set_search_set($set);
+
+        // get records
+        $result = $source->list_records(array('name', 'email'));
+
+        if (!$result->count) {
+            unset($search[$s]);
+            continue;
+        }
+
+        while ($row = $result->next()) {
+            $row['sourceid'] = $s;
+            $key = $row['name'] . ':' . $row['sourceid'];
+            $records[$key] = $row;
+        }
+        unset($result);
+
+        $search[$s] = $source->get_search_set();
+    }
+
+    $_SESSION['search'][$search_request] = $search;
+}
+
+$RCMAIL->session->remove('contact_undo');
+
+$OUTPUT->show_message('contactrestored', 'confirmation');
+$OUTPUT->command('list_contacts');
+
+// send response
+$OUTPUT->send();
diff --git a/skins/default/common.css b/skins/default/common.css
index 4f1ab6b..55d537c 100644
--- a/skins/default/common.css
+++ b/skins/default/common.css
@@ -252,6 +252,12 @@
   border: 1px solid #CCCCCC;
 }
 
+#message a
+{
+  cursor: pointer;
+  text-decoration: underline;
+}
+
 .box
 {
   border: 1px solid #999;
diff --git a/skins/default/templates/mail.html b/skins/default/templates/mail.html
index ea6a2f7..1645ba1 100644
--- a/skins/default/templates/mail.html
+++ b/skins/default/templates/mail.html
@@ -196,6 +196,6 @@
   <roundcube:button command="menu-save" id="listmenusave" type="input" class="button mainaction" label="save" />
 </div>
 </div>
-
+<div id="undelete-message">11 item(s) has been deleted. <a href="#">Undo</a></div>
 </body>
 </html>

--
Gitblit v1.9.1