1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 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, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates a filter rules object.
 26  * @constructor
 27  * @class
 28  * This class represents a set of filter rules. The rules are maintained in a {@link AjxVector}
 29  * and have an order. Each rule is a {@link ZmFilterRule}. Filter rules can be added and
 30  * edited via a {@link ZmFilterRuleDialog}.
 31  *
 32  * @author Conrad Damon
 33  *
 34  * @param {String}	accountName		the name of the account this set of filter rules belongs to
 35  * 
 36  * @extends		ZmModel
 37  */
 38 ZmFilterRules = function(accountName, outgoing) {
 39 
 40 	ZmModel.call(this, ZmEvent.S_FILTER);
 41 
 42 	this._vector = new AjxVector();
 43 	this._ruleIdHash = {};
 44 	this._ruleNameHash = {};
 45 	this._initialized = false;
 46 	this._accountName = accountName;
 47 	this._outgoing = outgoing;
 48 };
 49 
 50 ZmFilterRules.prototype = new ZmModel;
 51 ZmFilterRules.prototype.constructor = ZmFilterRules;
 52 
 53 ZmFilterRules.prototype.toString =
 54 function() {
 55 	return "ZmFilterRules";
 56 };
 57 
 58 /**
 59  * Adds a rule to the list.
 60  *
 61  * @param {ZmFilterRule}	rule			the rule to be added
 62  * @param {ZmFilterRule}	referenceRule	the rule after which to add the new rule
 63  * @param {AjxCallback}	callback		the callback
 64  */
 65 ZmFilterRules.prototype.addRule = 
 66 function(rule, referenceRule, callback) {
 67 	DBG.println(AjxDebug.DBG3, "FILTER RULES: add rule '" + rule.name + "'");
 68 	var index = referenceRule ? this._vector.indexOf(referenceRule) : 0;
 69 	this._insertRule(rule, index);
 70 	this._saveRules(index, true, callback);
 71 };
 72 
 73 /**
 74  * Removes a rule from the list.
 75  *
 76  * @param {ZmFilterRule}	rule			the rule to be removed
 77  */
 78 ZmFilterRules.prototype.removeRule = 
 79 function(rule) {
 80 	if (!rule) { return; }
 81 	DBG.println(AjxDebug.DBG3, "FILTER RULES: remove rule '" + rule.name + "'");
 82 	var index = this.getIndexOfRule(rule);
 83 	this._vector.removeAt(index);
 84 	delete this._ruleIdHash[rule.id];
 85 	delete this._ruleNameHash[rule.name];
 86 	this._deleteMode = true;
 87     this._saveRules(index, true);
 88 };
 89 
 90 /**
 91  * Moves a rule up in the list. If the rule is the first in the list, it isn't moved.
 92  *
 93  * @param {ZmFilterRule}	rule			the rule to be moved
 94  */
 95 ZmFilterRules.prototype.moveUp = 
 96 function(rule) {
 97 	if (!rule) { return; }
 98 	DBG.println(AjxDebug.DBG3, "FILTER RULES: move up rule '" + rule.name + "'");
 99 	var index = this.getIndexOfRule(rule);
100 	if (index == 0) { return; }
101 
102 	var prevRule = this._vector.removeAt(index - 1);
103 	this._insertRule(prevRule, index);
104 	this._saveRules(index - 1, true);
105 };
106 
107 /**
108  * Moves a rule down in the list. If the rule is the last in the list, it isn't moved.
109  *
110  * @param {ZmFilterRule}	rule			the rule to be moved
111  */
112 ZmFilterRules.prototype.moveDown = 
113 function(rule) {
114 	if (!rule) { return; }
115 	DBG.println(AjxDebug.DBG3, "FILTER RULES: move down rule '" + rule.name + "'");
116 	var index = this.getIndexOfRule(rule);
117 	if (index >= (this._vector.size() - 1)) { return; }
118 	
119 	var nextRule = this._vector.removeAt(index + 1);
120 	this._insertRule(nextRule, index);
121 	this._saveRules(index + 1, true);
122 };
123 
124 /**
125  * Moves a rule to the bottom of the list.  If the rule is the last in the list, it isn't moved.
126  * @param rule  {ZmFilterRule}  rule    the rule to be moved
127  * @param skipSave  {boolean}   true to not save
128  */
129 ZmFilterRules.prototype.moveToBottom = 
130 function(rule, skipSave) {
131 	if (!rule) { return; }
132 	var index = this.getIndexOfRule(rule);
133 	if (index >= (this._vector.size() - 1)) { return; }
134 	
135 	while (index < this._vector.size() -1) {
136 		var nextRule = this._vector.removeAt(index+1);
137 		this._insertRule(nextRule, index);
138 		index++;
139 	}
140 	if (!skipSave) {
141 		this._saveRules(index, true);
142 	}
143 };
144 
145 /**
146  * Marks a rule as active/inactive.
147  *
148  * @param {ZmFilterRule}	rule			the rule to mark active/inactive
149  * @param {Boolean}	active			if <code>true</code>, the rule is marked active
150  */
151 ZmFilterRules.prototype.setActive =
152 function(rule, active) {
153 	if (!rule) { return; }
154 	DBG.println(AjxDebug.DBG3, "FILTER RULES: set active rule '" + rule.name + "', " + active);
155 	rule.active = active;
156 	this._saveRules(null, false);
157 };
158 
159 // utility methods
160 
161 /**
162  * Gets the number of rules in the list.
163  * 
164  * @return	{int}		the number of rules
165  */
166 ZmFilterRules.prototype.getNumberOfRules = 
167 function() {
168 	return this._vector.size();
169 };
170 
171 /**
172  * Gets the active rules in the list.
173  * 
174  * @return	{AjxVector}		the active rules
175  */
176 ZmFilterRules.prototype.getActiveRules = 
177 function() {
178 	return this._vector.sub(function(rule){return !rule.active});
179 };
180 
181 /**
182  * Gets the numeric index of the rule in the list.
183  *
184  * @param {ZmFilterRule}	rule	a rule
185  * @return	{int}	the index
186  */
187 ZmFilterRules.prototype.getIndexOfRule = 
188 function(rule) {
189 	return this._vector.indexOf(rule);
190 };
191 
192 /**
193  * Gets a rule based on its index.
194  *
195  * @param {int}		index		the index
196  * @return	{ZmFilterRule}	the rule
197  */
198 ZmFilterRules.prototype.getRuleByIndex = 
199 function(index) {
200     return this._vector.get(index);
201 };
202 
203 /**
204  * Gets a rule based on its ID.
205  *
206  * @param {String}	id		the rule ID
207  * @return	{ZmFilterRule}	the rule
208  */
209 ZmFilterRules.prototype.getRuleById = 
210 function(id) {
211 	return this._ruleIdHash[id];
212 };
213 
214 /**
215  * Gets a rule by name.
216  *
217  * @param {String}	name	the rule name
218  * @return	{ZmFilterRule}	the rule
219  */
220 ZmFilterRules.prototype.getRuleByName = 
221 function(name) {
222 	return this._ruleNameHash[name];
223 };
224 
225 ZmFilterRules.prototype.getOutgoing = 
226 function(name) {
227 	return this._outgoing
228 };
229 
230 /**
231  * Loads the rules from the server.
232  *
233  * @param {Boolean}	force			if <code>true</code>, get rules from server
234  * @param {AjxCallback}	callback		the callback
235  */
236 ZmFilterRules.prototype.loadRules = 
237 function(force, callback) {
238 	// return cache?
239 	if (this._initialized && !force) {
240 		if (callback) {
241 			callback.run(new ZmCsfeResult(this._vector));
242 			return;
243 		}
244 		return this._vector;
245 	}
246 
247 	// fetch from server:
248 	DBG.println(AjxDebug.DBG3, "FILTER RULES: load rules");
249 	var params = {
250 		soapDoc: AjxSoapDoc.create(this._outgoing ? "GetOutgoingFilterRulesRequest" : "GetFilterRulesRequest", "urn:zimbraMail"),
251 		asyncMode: true,
252 		callback: (new AjxCallback(this, this._handleResponseLoadRules, [callback])),
253 		accountName:this._accountName
254 	};
255 	appCtxt.getAppController().sendRequest(params);
256 };
257 
258 ZmFilterRules.prototype._handleResponseLoadRules =
259 function(callback, result) {
260 	this._vector.removeAll();
261 	this._ruleIdHash = {};
262 	this._ruleNameHash = {};
263 
264 	var r = result.getResponse();
265 	var resp = this._outgoing ? r.GetOutgoingFilterRulesResponse : r.GetFilterRulesResponse;
266 	var children = resp.filterRules[0].filterRule;
267 	if (children) {
268 		for (var i = 0; i < children.length; i++) {
269 			var ruleNode = children[i];
270 			var rule = new ZmFilterRule(ruleNode.name, ruleNode.active, ruleNode.filterActions[0], ruleNode.filterTests[0]);
271 			this._insertRule(rule);
272 		}
273 	}
274 
275 	this._initialized = true;
276 
277 	if (callback) {
278 		result.set(this._vector);
279 		callback.run(result);
280 	} else {
281 		return this._vector;
282 	}
283 };
284 
285 /**
286  * Public method to save the rules to the server.
287  *
288  * @param {int}	index			the index of rule to select in list after save
289  * @param {Boolean}	notify			if <code>true</code>, notify listeners of change event
290  * @param {AjxCallback}	callback		the callback
291  * 
292  * @public
293  */
294 ZmFilterRules.prototype.saveRules = 
295 function(index, notify, callback) {
296 	this._saveRules(index, notify, callback);	
297 };
298 
299 /**
300  * Saves the rules to the server.
301  *
302  * @param {int}	index			the index of rule to select in list after save
303  * @param {Boolean}	notify			if <code>true</code>, notify listeners of change event
304  * @param {AjxCallback}	callback		the callback
305  * 
306  * @private
307  */
308 ZmFilterRules.prototype._saveRules = 
309 function(index, notify, callback) {
310 	var requestKey = this._outgoing ? "ModifyOutgoingFilterRulesRequest" : "ModifyFilterRulesRequest";
311 	var jsonObj = {};
312 	jsonObj[requestKey] = {_jsns:"urn:zimbraMail"};
313 
314 	var request = jsonObj[requestKey];
315 
316 	var rules = this._vector.getArray();
317 	if (rules.length > 0) {
318 		request.filterRules = [{filterRule:[]}];
319 		var filterRuleObj = request.filterRules[0].filterRule;
320 
321 		for (var i = 0; i < rules.length; i++) {
322 			var r = rules[i];
323 			var ruleObj = {
324 				active: r.active,
325 				name: r.name,
326 				filterActions: [],
327 				filterTests: []
328 			};
329 			ruleObj.filterActions.push(r.actions);
330 			ruleObj.filterTests.push(r.conditions);
331 			filterRuleObj.push(ruleObj);
332 		}
333 	} else {
334 		request.filterRules = {};
335 	}
336 
337 	var params = {
338 		jsonObj: jsonObj,
339 		asyncMode: true,
340 		callback: (new AjxCallback(this, this._handleResponseSaveRules, [index, notify, callback])),
341 		errorCallback: (new AjxCallback(this, this._handleErrorSaveRules)),
342 		accountName: this._accountName
343 	};
344 	appCtxt.getAppController().sendRequest(params);
345 };
346 
347 ZmFilterRules.prototype._handleResponseSaveRules =
348 function(index, notify, callback, result) {
349 	if (notify) {
350 		this._notify(ZmEvent.E_MODIFY, {index: index});
351 	}
352 
353     if(this._deleteMode){
354         appCtxt.setStatusMsg(ZmMsg.filtersDeleted);
355         this._deleteMode = false;
356     }
357 
358     else if(appCtxt.getFilterRuleDialog().isEditMode()){
359 	    appCtxt.setStatusMsg(ZmMsg.filtersEdited);
360     }
361     else{
362         appCtxt.setStatusMsg(ZmMsg.filtersSaved);
363     }
364 
365 	if (callback) {
366 		callback.run(result);
367 	}
368 };
369 
370 /**
371  * The save failed. Show an error dialog.
372  * @param {AjxException}	ex		the exception
373  * 
374  * @private
375  */
376 ZmFilterRules.prototype._handleErrorSaveRules =
377 function(ex) {
378 	if (ex.code == ZmCsfeException.SVC_PARSE_ERROR ||
379 		ex.code == ZmCsfeException.SVC_INVALID_REQUEST)
380 	{
381 		var msgDialog = appCtxt.getMsgDialog();
382 		msgDialog.setMessage([ZmMsg.filterError, " ", AjxStringUtil.htmlEncode(ex.msg)].join(""), DwtMessageDialog.CRITICAL_STYLE);
383 		msgDialog.popup();
384         //only reload rules if the filter rule dialog is not popped up or if a new rule is being added
385         //get index for refreshing list view
386         if (!appCtxt.getFilterRuleDialog() || !appCtxt.getFilterRuleDialog().isPoppedUp() || !appCtxt.getFilterRuleDialog().isEditMode()) {
387             var filterRulesController = ZmPreferencesApp.getFilterRulesController(this._outgoing);
388             var sel = filterRulesController.getListView() ? filterRulesController.getListView().getSelection()[0] : null;
389             var index = sel ? this.getIndexOfRule(sel) - 1: null;   //new filter is inserted into vector, subtract 1 to get the selected index
390 		    var respCallback = new AjxCallback(this, this._handleResponseHandleErrorSaveRules, [index]);
391 		    this.loadRules(true, respCallback);
392         }
393 		return true;
394 	}
395 	return false;
396 };
397 
398 // XXX: the caller should probably be the one doing this
399 ZmFilterRules.prototype._handleResponseHandleErrorSaveRules =
400 function(index) {
401 	var prefController = AjxDispatcher.run("GetPrefController");
402 	var prefsView = prefController.getPrefsView();
403 	var section = ZmPref.getPrefSectionWithPref(ZmSetting.FILTERS);
404 	if (section && prefsView && prefsView.getView(section.id)) {
405         var filterRulesController = ZmPreferencesApp.getFilterRulesController(this._outgoing);
406 		filterRulesController.resetListView(index);
407 	}
408 };
409 
410 /**
411  * Inserts a rule into the internal vector. Adds to the end if no index is given.
412  *
413  * @param {ZmFilterRule}	rule		the rule to insert
414  * @param {int}	index		the index at which to insert
415  * 
416  * @private
417  */
418 ZmFilterRules.prototype._insertRule = 
419 function(rule, index) {
420 	this._vector.add(rule, index);
421 	this._ruleIdHash[rule.id] = rule;
422 	this._ruleNameHash[rule.name] = rule;
423 };
424 
425 /**
426  * Public method to insert rule into internval vectors.  Adds to the end if no index is given.
427  * 
428  * @param {ZmFilterRule}	rule		the rule to insert
429  * @param {int}	index		the index at which to insert
430  * 
431  * @public
432  */
433 ZmFilterRules.prototype.insertRule =
434 function(rule, index) {
435 	this._insertRule(rule, index);	
436 };