1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * Creates a new, empty filter rules controller. 26 * @class 27 * This class represents the filter rules controller. This controller manages 28 * the filter rules page, which has a button toolbar and a list view of the rules. 29 * 30 * @author Conrad Damon 31 * 32 * @param {DwtShell} container the shell 33 * @param {ZmPreferencesApp} prefsApp the preferences application 34 * 35 * @extends ZmController 36 */ 37 ZmFilterRulesController = function(container, prefsApp, prefsView, parent, outgoing) { 38 39 ZmController.call(this, container, prefsApp); 40 41 this._prefsView = prefsView; 42 this._parent = parent; 43 44 this._filterRulesView = new ZmFilterRulesView(this._prefsView, this); 45 46 this._outgoing = Boolean(outgoing); 47 48 this._buttonListeners = {}; 49 this._buttonListeners[ZmOperation.ADD_FILTER_RULE] = new AjxListener(this, this._addListener); 50 this._buttonListeners[ZmOperation.EDIT_FILTER_RULE] = new AjxListener(this, this._editListener); 51 this._buttonListeners[ZmOperation.REMOVE_FILTER_RULE] = new AjxListener(this, this._removeListener); 52 this._buttonListeners[ZmOperation.RUN_FILTER_RULE] = new AjxListener(this, this._runListener); 53 this._progressController = new ZmProgressController(container, prefsApp); 54 55 // reset community name since it gets its value from a setting 56 ZmFilterRule.C_LABEL[ZmFilterRule.C_COMMUNITY] = ZmMsg.communityName; 57 }; 58 59 ZmFilterRulesController.prototype = new ZmController(); 60 ZmFilterRulesController.prototype.constructor = ZmFilterRulesController; 61 62 ZmFilterRulesController.prototype.toString = 63 function() { 64 return "ZmFilterRulesController"; 65 }; 66 67 ZmFilterRulesController.prototype.isOutgoing = 68 function() { 69 return this._outgoing; 70 }; 71 72 /** 73 * Gets the filter rules view, which is comprised of a toolbar and a list view. 74 * 75 * @return {ZmFilterRulesView} the filter rules view 76 */ 77 ZmFilterRulesController.prototype.getFilterRulesView = 78 function() { 79 return this._filterRulesView; 80 }; 81 82 /** 83 * Initializes the controller. 84 * 85 * @param {ZmToolBar} toolbar the toolbar 86 * @param {ZmListView} listView active list view 87 * @param {ZmListView} listView not active list view 88 */ 89 ZmFilterRulesController.prototype.initialize = 90 function(toolbar, listView, notActiveListView) { 91 // always reset the the rules to make sure we get the right one for the *active* account 92 this._rules = AjxDispatcher.run(this._outgoing ? "GetOutgoingFilterRules" : "GetFilterRules"); 93 94 if (toolbar) { 95 var buttons = this.getToolbarButtons(); 96 for (var i = 0; i < buttons.length; i++) { 97 var id = buttons[i]; 98 if (this._buttonListeners[id]) { 99 toolbar.addSelectionListener(id, this._buttonListeners[id]); 100 } 101 } 102 this._resetOperations(toolbar, 0); 103 } 104 105 if (notActiveListView) { 106 this._notActiveListView = notActiveListView; 107 notActiveListView.addSelectionListener(new AjxListener(this, this._listSelectionListener)); 108 notActiveListView.addActionListener(new AjxListener(this, this._listActionListener)); 109 this.resetListView(0); 110 } 111 112 if (listView) { 113 this._listView = listView; 114 listView.addSelectionListener(new AjxListener(this, this._listSelectionListener)); 115 listView.addActionListener(new AjxListener(this, this._listActionListener)); 116 this.resetListView(0); 117 } 118 119 }; 120 121 ZmFilterRulesController.prototype.getRules = 122 function() { 123 if (!this._rules) 124 this._rules = AjxDispatcher.run(this._outgoing ? "GetOutgoingFilterRules" : "GetFilterRules"); 125 return this._rules; 126 }; 127 128 ZmFilterRulesController.prototype.getToolbarButtons = 129 function() { 130 var ops = [ 131 ZmOperation.ADD_FILTER_RULE, 132 ZmOperation.SEP, 133 ZmOperation.EDIT_FILTER_RULE, 134 ZmOperation.SEP, 135 ZmOperation.REMOVE_FILTER_RULE 136 ]; 137 138 // bug: 42903 - disable running filters in offline for now 139 if (!appCtxt.isOffline) { 140 ops.push(ZmOperation.SEP, ZmOperation.RUN_FILTER_RULE); 141 } 142 143 return ops; 144 }; 145 146 ZmFilterRulesController.prototype.resetListView = 147 function(selectedIndex) { 148 if (!this._listView) { return; } 149 150 var respCallback = new AjxCallback(this, this._handleResponseSetListView, [selectedIndex]); 151 this._rules.loadRules(true, respCallback); //bug 37339 - filters don't show newly created filter 152 }; 153 154 ZmFilterRulesController.prototype._handleResponseSetListView = 155 function(selectedIndex, result) { 156 this._listView.set(result.getResponse().clone()); 157 this._notActiveListView.set(result.getResponse().clone()); 158 var rule = this._rules.getRuleByIndex(selectedIndex || 0); 159 if (rule && rule.active) { 160 this._listView.setSelection(rule); 161 } 162 else if (rule) { 163 this._notActiveListView.setSelection(rule); 164 } 165 }; 166 167 /** 168 * Handles left-clicking on a rule. Double click opens up a rule for editing. 169 * 170 * @param {DwtEvent} ev the click event 171 * 172 * @private 173 */ 174 ZmFilterRulesController.prototype._listSelectionListener = 175 function(ev) { 176 var listView = this.getListView(); 177 if (ev.detail == DwtListView.ITEM_DBL_CLICKED) { 178 this._editListener(ev); 179 } else { 180 var tb = this._filterRulesView.getToolbar(); 181 this._resetOperations(tb, listView.getSelectionCount(), listView.getSelection()); 182 } 183 }; 184 185 ZmFilterRulesController.prototype._listActionListener = 186 function(ev) { 187 var listView = this.getListView(); 188 var actionMenu = this.getActionMenu(); 189 this._resetOperations(actionMenu, listView.getSelectionCount(), listView.getSelection()); 190 actionMenu.popup(0, ev.docX, ev.docY); 191 }; 192 193 /** 194 * Gets the action menu. 195 * 196 * @return {ZmActionMenu} the action menu 197 */ 198 ZmFilterRulesController.prototype.getActionMenu = 199 function() { 200 if (!this._actionMenu) { 201 this._initializeActionMenu(); 202 var listView = this.getListView(); 203 this._resetOperations(this._actionMenu, 0, listView.getSelection()); 204 } 205 return this._actionMenu; 206 }; 207 208 // action menu: menu items and listeners 209 ZmFilterRulesController.prototype._initializeActionMenu = 210 function() { 211 if (this._actionMenu) { return; } 212 213 var menuItems = this._getActionMenuOps(); 214 if (menuItems) { 215 var params = { 216 parent:this._shell, 217 menuItems:menuItems, 218 context:this._getMenuContext(), 219 controller:this 220 }; 221 this._actionMenu = new ZmActionMenu(params); 222 this._addMenuListeners(this._actionMenu); 223 } 224 }; 225 226 ZmFilterRulesController.prototype._getActionMenuOps = 227 function() { 228 var ops = [ 229 ZmOperation.EDIT_FILTER_RULE, 230 ZmOperation.REMOVE_FILTER_RULE 231 ]; 232 233 // bug: 42903 - disable running filters in offline for now 234 if (!appCtxt.isOffline) { 235 ops.push(ZmOperation.RUN_FILTER_RULE); 236 } 237 238 ops.push(ZmOperation.SEP, 239 ZmOperation.MOVE_UP_FILTER_RULE, 240 ZmOperation.MOVE_DOWN_FILTER_RULE 241 ); 242 243 return ops; 244 }; 245 246 /** 247 * Returns the context for the action menu created by this controller (used to create 248 * an ID for the menu). 249 */ 250 ZmFilterRulesController.prototype._getMenuContext = 251 function() { 252 return this._app && this._app._name; 253 }; 254 255 ZmFilterRulesController.prototype._addMenuListeners = 256 function(menu) { 257 var menuItems = menu.opList; 258 for (var i = 0; i < menuItems.length; i++) { 259 var menuItem = menuItems[i]; 260 if (this._buttonListeners[menuItem]) { 261 menu.addSelectionListener(menuItem, this._buttonListeners[menuItem], 0); 262 } 263 } 264 menu.addPopdownListener(this._menuPopdownListener); 265 }; 266 267 /** 268 * The "Add Filter" button has been pressed. 269 * 270 * @ev [DwtEvent] the click event 271 */ 272 ZmFilterRulesController.prototype._addListener = 273 function(ev) { 274 var listView = this.getListView(); 275 if (!listView) { return; } 276 this.handleBeforeFilterChange(new AjxCallback(this, this._popUpAdd)); 277 }; 278 279 ZmFilterRulesController.prototype.handleBeforeFilterChange = 280 function(okCallback, cancelCallback) { 281 if (this._outgoing && (appCtxt.getSettings().getSetting(ZmSetting.SAVE_TO_SENT).getValue()===false || ZmPref.getFormValue(ZmSetting.SAVE_TO_SENT)===false)) { 282 var dialog = appCtxt.getConfirmationDialog(); 283 if (!this._saveToSentMessage) { 284 var html = []; 285 var i = 0; 286 html[i++] = "<table cellspacing=0 cellpadding=0 border=0><tr><td valign='top'>"; 287 html[i++] = AjxImg.getImageHtml("Warning_32"); 288 html[i++] = "</td><td class='DwtMsgArea'>"; 289 html[i++] = ZmMsg.filterOutgoingNoSaveToSentWarning; 290 html[i++] = "</td></tr></table>"; 291 this._saveToSentMessage = html.join(""); 292 } 293 var handleSaveToSentYesListener = new AjxListener(this, this._handleSaveToSentYes, [okCallback]); 294 var handleSaveToSentNoListener = new AjxListener(this, this._handleSaveToSentNo, [okCallback]); 295 296 dialog.popup(this._saveToSentMessage, handleSaveToSentYesListener, handleSaveToSentNoListener, cancelCallback); 297 dialog.setTitle(AjxMsg.warningMsg); 298 } else { 299 if (okCallback) 300 okCallback.run(); 301 } 302 }; 303 304 ZmFilterRulesController.prototype._handleSaveToSentYes = 305 function(callback) { 306 var settings = appCtxt.getSettings(); 307 var setting = settings.getSetting(ZmSetting.SAVE_TO_SENT); 308 ZmPref.setFormValue(ZmSetting.SAVE_TO_SENT, true); 309 if (!setting.getValue()) { 310 setting.setValue(true); 311 settings.save([setting], callback); 312 } else { 313 if (callback) 314 callback.run(); 315 } 316 }; 317 318 ZmFilterRulesController.prototype._handleSaveToSentNo = 319 function(callback) { 320 if (callback) 321 callback.run(); 322 }; 323 324 ZmFilterRulesController.prototype._popUpAdd = 325 function() { 326 var listView = this.getListView(); 327 var sel = listView.getSelection(); 328 var refRule = sel.length ? sel[sel.length - 1] : null; 329 appCtxt.getFilterRuleDialog().popup(null, false, refRule, null, this._outgoing); 330 }; 331 332 /** 333 * The "Edit Filter" button has been pressed. 334 * 335 * @ev [DwtEvent] the click event 336 */ 337 ZmFilterRulesController.prototype._editListener = 338 function(ev) { 339 var listView = this.getListView(); 340 if (!listView) { return; } 341 342 var sel = listView.getSelection(); 343 appCtxt.getFilterRuleDialog().popup(sel[0], true, null, null, this._outgoing); 344 }; 345 346 /** 347 * The "Delete Filter" button has been pressed. 348 * 349 * @ev [DwtEvent] the click event 350 */ 351 ZmFilterRulesController.prototype._removeListener = 352 function(ev) { 353 var listView = this.getListView(); 354 if (!listView) { return; } 355 var sel = listView.getSelection(); 356 var rule = sel[0]; 357 //bug:16053 changed getYesNoCancelMsgDialog to getYesNoMsgDialog 358 var ds = this._deleteShield = appCtxt.getYesNoMsgDialog(); 359 ds.reset(); 360 ds.registerCallback(DwtDialog.NO_BUTTON, this._clearDialog, this, this._deleteShield); 361 ds.registerCallback(DwtDialog.YES_BUTTON, this._deleteShieldYesCallback, this, rule); 362 var msg = AjxMessageFormat.format(ZmMsg.askDeleteFilter, AjxStringUtil.htmlEncode(rule.name)); 363 ds.setMessage(msg, DwtMessageDialog.WARNING_STYLE); 364 ds.popup(); 365 }; 366 367 ZmFilterRulesController.prototype._runListener = 368 function(ev) { 369 // !!! do *NOT* get choose folder dialog from appCtxt since this one has checkboxes! 370 if (!this._chooseFolderDialog) { 371 AjxDispatcher.require("Extras"); 372 this._chooseFolderDialog = new ZmChooseFolderDialog(appCtxt.getShell()); 373 } 374 this._chooseFolderDialog.reset(); 375 this._chooseFolderDialog.registerCallback(DwtDialog.OK_BUTTON, this._runFilterOkCallback, this, this._chooseFolderDialog); 376 377 // bug 42725: always omit shared folders 378 var omit = {}; 379 var tree = appCtxt.getTree(ZmOrganizer.FOLDER); 380 var children = tree.root.children.getArray(); 381 for (var i = 0; i < children.length; i++) { 382 var child = children[i]; 383 if (child.type == ZmOrganizer.FOLDER && child.isRemote()) { 384 omit[child.id] = true; 385 } 386 } 387 388 var params = { 389 treeIds: [ZmOrganizer.FOLDER], 390 title: ZmMsg.chooseFolder, 391 overviewId: this.toString() + (this._outgoing ? "_outgoing":"_incoming"), 392 description: ZmMsg.chooseFolderToFilter, 393 skipReadOnly: true, 394 hideNewButton: true, 395 treeStyle: DwtTree.CHECKEDITEM_STYLE, 396 appName: ZmApp.MAIL, 397 omit: omit 398 }; 399 this._chooseFolderDialog.popup(params); 400 401 var foundForwardAction; 402 var listView = this.getListView(); 403 var sel = listView && listView.getSelection(); 404 for (var i = 0; i < sel.length; i++) { 405 if (sel[i].actions[ZmFilterRule.A_NAME_FORWARD]) { 406 foundForwardAction = true; 407 break; 408 } 409 } 410 411 if (foundForwardAction) { 412 var dialog = appCtxt.getMsgDialog(); 413 dialog.setMessage(ZmMsg.filterForwardActionWarning); 414 dialog.popup(); 415 } 416 }; 417 418 ZmFilterRulesController.prototype._runFilterOkCallback = 419 function(dialog, folderList) { 420 dialog.popdown(); 421 var listView = this.getListView(); 422 var filterSel = listView && listView.getSelection(); 423 if (!(filterSel && filterSel.length)) { 424 return; 425 } 426 427 // Bug 78392: We need the selection sorted 428 if (filterSel.length > 1) { 429 var list = this._listView.getList().getArray(); 430 var selectedIds = {}, sortedSelection = []; 431 for (var i=0; i<filterSel.length; i++) { 432 selectedIds[filterSel[i].id] = true; 433 } 434 for (var i=0; i<list.length; i++) { 435 if (selectedIds[list[i].id]) { 436 sortedSelection.push(list[i]); 437 } 438 } 439 filterSel = sortedSelection; 440 } 441 442 var work = new ZmFilterWork(filterSel, this._outgoing); 443 444 this._progressController.start(folderList, work); 445 446 }; 447 448 /** 449 * runs a specified list of filters 450 * 451 * @param container {DwtControl} container reference 452 * @param filterSel {Array} array of ZmFilterRule 453 * @param isOutgoing {Boolean} 454 */ 455 ZmFilterRulesController.prototype.runFilter = 456 function(container, filterSel, isOutgoing) { 457 var work = new ZmFilterWork(filterSel, isOutgoing); 458 this._progressController.start(container, work); 459 }; 460 461 /** 462 * The user has agreed to delete a filter rule. 463 * 464 * @param rule [ZmFilterRule] rule to delete 465 */ 466 ZmFilterRulesController.prototype._deleteShieldYesCallback = 467 function(rule) { 468 this._rules.removeRule(rule); 469 this._clearDialog(this._deleteShield); 470 this._resetOperations(this._filterRulesView.getToolbar(), 0); 471 }; 472 473 /** 474 * The "Move Up" button has been pressed. 475 * 476 * @param ev [DwtEvent] the click event 477 */ 478 ZmFilterRulesController.prototype.moveUpListener = 479 function(ev) { 480 var listView = this.getListView(); 481 if (!listView) { return; } 482 483 var sel = listView.getSelection(); 484 this._rules.moveUp(sel[0]); 485 }; 486 487 /** 488 * The "Move Down" button has been pressed. 489 * 490 * @ev [DwtEvent] the click event 491 */ 492 ZmFilterRulesController.prototype.moveDownListener = 493 function(ev) { 494 var listView = this.getListView(); 495 if (!listView) { return; } 496 497 var sel = listView.getSelection(); 498 this._rules.moveDown(sel[0]); 499 }; 500 501 /** 502 * Resets the toolbar button states, depending on which rule is selected. 503 * The list view enforces single selection only. If the first rule is selected, 504 * "Move Up" is disabled. Same for last rule and "Move Down". They're both 505 * disabled if there aren't at least two rules. 506 * 507 * @param parent [ZmButtonToolBar] the toolbar 508 * @param numSel [int] number of rules selected (0 or 1) 509 * @param sel [Array] list of selected rules 510 */ 511 ZmFilterRulesController.prototype._resetOperations = 512 function(parent, numSel, sel) { 513 var numRules = this._rules.getNumberOfRules(); 514 if (numSel == 1) { 515 parent.enableAll(true); 516 } else { 517 parent.enableAll(false); 518 parent.enable(ZmOperation.ADD_FILTER_RULE, true); 519 if (numSel > 1) { 520 parent.enable(ZmOperation.RUN_FILTER_RULE, true); 521 } 522 } 523 524 if (numRules == 0) { 525 parent.enable(ZmOperation.EDIT_FILTER_RULE, false); 526 parent.enable(ZmOperation.REMOVE_FILTER_RULE, false); 527 } 528 }; 529 530 ZmFilterRulesController.prototype.getListView = 531 function(){ 532 if (this._listView && this._notActiveListView) { 533 var activeSel = this._listView.getSelection(); 534 var notActiveSel = this._notActiveListView.getSelection(); 535 if (!AjxUtil.isEmpty(activeSel)) { 536 return this._listView; 537 } 538 else if (!AjxUtil.isEmpty(notActiveSel)) { 539 return this._notActiveListView; 540 } 541 } 542 return this._listView; 543 }; 544 545 546 547 /** 548 * class that holds the work specification (in this case, filtering specific filters. Keeps track of progress stats too. 549 * an instance of this is passed to ZmFilterRulesController to callback for stuff specific to this work. (template pattern, I believe) 550 * @param filterSel 551 * @param outgoing 552 */ 553 ZmFilterWork = function(filterSel, outgoing) { 554 this._filterSel = filterSel; 555 this._outgoing = outgoing; 556 this._totalNumMessagesAffected = 0; 557 558 }; 559 560 /** 561 * return the summary message when finished everything. 562 */ 563 ZmFilterWork.prototype.getFinishedMessage = 564 function(messagesProcessed) { 565 if (messagesProcessed) { 566 return AjxMessageFormat.format(ZmMsg.filterRuleApplied, [messagesProcessed, this._totalNumMessagesAffected]); 567 } 568 else { 569 return AjxMessageFormat.format(ZmMsg.filterRuleAppliedBackground, [this._totalNumMessagesAffected]); 570 } 571 }; 572 573 /** 574 * return the progress so far summary. 575 */ 576 ZmFilterWork.prototype.getProgressMessage = 577 function(messagesProcessed) { 578 return AjxMessageFormat.format(ZmMsg.filterRunInProgress, [messagesProcessed, this._totalNumMessagesAffected]); 579 }; 580 581 /** 582 * return the finished dialog title. 583 */ 584 ZmFilterWork.prototype.getFinishedTitle = 585 function(messagesProcessed) { 586 return AjxMessageFormat.format(ZmMsg.filterRunFinished); 587 }; 588 589 /** 590 * return the progress dialog title. 591 */ 592 ZmFilterWork.prototype.getProgressTitle = 593 function(messagesProcessed) { 594 return AjxMessageFormat.format(ZmMsg.filterRunInProgressTitle); 595 }; 596 597 598 /** 599 * do the work. (in this case apply filters). Either msgIds or query should be set but not both. 600 * @param msgIds {String} chunk of message ids to do the work on. 601 * @param query {String} query to run filter against 602 * @param callback 603 */ 604 ZmFilterWork.prototype.doWork = 605 function(msgIds, query, callback) { 606 var filterSel = this._filterSel; 607 var soapDoc = AjxSoapDoc.create(this._outgoing ? "ApplyOutgoingFilterRulesRequest" : "ApplyFilterRulesRequest", "urn:zimbraMail"); 608 var filterRules = soapDoc.set("filterRules", null); 609 for (var i = 0; i < filterSel.length; i++) { 610 var rule = soapDoc.set("filterRule", null, filterRules); 611 rule.setAttribute("name", filterSel[i].name); 612 } 613 var noBusyOverlay = false; 614 if (msgIds) { 615 var m = soapDoc.set("m"); 616 m.setAttribute("ids", msgIds.join(",")); 617 } 618 else { 619 soapDoc.set("query", query); 620 noBusyOverlay = true; 621 } 622 623 var params = { 624 soapDoc: soapDoc, 625 asyncMode: true, 626 noBusyOverlay: noBusyOverlay, 627 callback: (new AjxCallback(this, this._handleRunFilter, [callback])) 628 }; 629 appCtxt.getAppController().sendRequest(params); 630 }; 631 632 /** 633 * private method - gets the result of the filter request, and keeps track of total messages affected. 634 * @param callback 635 * @param result 636 */ 637 ZmFilterWork.prototype._handleRunFilter = 638 function(callback, result) { 639 var r = result.getResponse(); 640 var resp = this._outgoing ? r.ApplyOutgoingFilterRulesResponse : r.ApplyFilterRulesResponse; 641 var num = (resp && resp.m && resp.m.length) 642 ? (resp.m[0].ids.split(",").length) : 0; 643 this._totalNumMessagesAffected += num; 644 callback.run(); 645 }; 646 647 648