1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2004, 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) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 * 27 * This file defines a folder tree. 28 * 29 */ 30 31 /** 32 * Creates an empty folder tree. 33 * @class 34 * This class represents a tree of folders. It may be typed, in which case 35 * the folders are all of that type, or untyped. 36 * 37 * @author Conrad Damon 38 * 39 * @param {constant} type the organizer type 40 * 41 * @extends ZmTree 42 */ 43 ZmFolderTree = function(type) { 44 ZmTree.call(this, type); 45 }; 46 47 ZmFolderTree.prototype = new ZmTree; 48 ZmFolderTree.prototype.constructor = ZmFolderTree; 49 50 51 // Consts 52 ZmFolderTree.IS_PARSED = {}; 53 54 55 // Public Methods 56 57 /** 58 * Returns a string representation of the object. 59 * 60 * @return {String} a string representation of the object 61 */ 62 ZmFolderTree.prototype.toString = 63 function() { 64 return "ZmFolderTree"; 65 }; 66 67 /** 68 * Loads the folder or the zimlet tree. 69 * 70 * @param {Object} rootObj the root object 71 * @param {String} elementType the element type 72 * @param {ZmZimbraAccount} account the account 73 */ 74 ZmFolderTree.prototype.loadFromJs = 75 function(rootObj, elementType, account) { 76 this.root = (elementType == "zimlet") 77 ? ZmZimlet.createFromJs(null, rootObj, this) 78 : ZmFolderTree.createFromJs(null, rootObj, this, elementType, null, account); 79 }; 80 81 /** 82 * Generic function for creating a folder. Handles any organizer type that comes 83 * in the folder list. 84 * 85 * @param {ZmFolder} parent the parent folder 86 * @param {Object} obj the JSON with folder data 87 * @param {ZmFolderTree} tree the containing tree 88 * @param {String} elementType the type of containing JSON element 89 * @param {Array} path the list of path elements 90 * @param {ZmZimbraAccount} account the account this folder belongs to 91 */ 92 ZmFolderTree.createFromJs = 93 function(parent, obj, tree, elementType, path, account) { 94 if (!(obj && obj.id)) { return; } 95 96 var folder; 97 if (elementType == "search") { 98 var types; 99 var idParts = obj.id.split(":"); 100 // Suppress display of searches for the shared mailbox (See Bug 96090) - it will have an id 101 // of the form 'uuid:id'. Local searches will just have 'id' 102 if (!idParts || (idParts.length <= 1)) { 103 if (obj.types) { 104 var t = obj.types.split(","); 105 types = []; 106 var mailEnabled = appCtxt.get(ZmSetting.MAIL_ENABLED); 107 for (var i = 0; i < t.length; i++) { 108 var type = ZmSearch.TYPE_MAP[t[i]]; 109 if (!type || (!mailEnabled && (type == ZmItem.CONV || type == ZmItem.MSG))) { 110 continue; 111 } 112 types.push(type); 113 } 114 if (types.length == 0) { 115 return null; 116 } 117 } 118 DBG.println(AjxDebug.DBG2, "Creating SEARCH with id " + obj.id + " and name " + obj.name); 119 var params = { 120 id: obj.id, 121 name: obj.name, 122 parent: parent, 123 tree: tree, 124 numUnread: obj.u, 125 query: obj.query, 126 types: types, 127 sortBy: obj.sortBy, 128 account: account, 129 color: obj.color, 130 rgb: obj.rgb 131 }; 132 folder = new ZmSearchFolder(params); 133 ZmFolderTree._fillInFolder(folder, obj, path); 134 ZmFolderTree._traverse(folder, obj, tree, (path || []), elementType, account); 135 } 136 } else { 137 var type = obj.view 138 ? (ZmOrganizer.TYPE[obj.view]) 139 : (parent ? parent.type : ZmOrganizer.FOLDER); 140 141 if (!type) { 142 DBG.println(AjxDebug.DBG1, "No known type for view " + obj.view); 143 return; 144 } 145 // let's avoid deferring folders for offline since multi-account folder deferring is hairy 146 var hasGrants = (obj.acl && obj.acl.grant && obj.acl.grant.length > 0); 147 if (appCtxt.inStartup && ZmOrganizer.DEFERRABLE[type] && !appCtxt.isOffline) { 148 var app = appCtxt.getApp(ZmOrganizer.APP[type]); 149 var defParams = { 150 type: type, 151 parent: parent, 152 obj: obj, 153 tree: tree, 154 path: path, 155 elementType: elementType, 156 account: account 157 }; 158 app.addDeferredFolder(defParams); 159 } else { 160 var pkg = ZmOrganizer.ORG_PACKAGE[type]; 161 if (pkg) { 162 AjxDispatcher.require(pkg); 163 } 164 folder = ZmFolderTree.createFolder(type, parent, obj, tree, path, elementType, account); 165 if (appCtxt.isExternalAccount() && folder.isSystem() && folder.id != ZmOrganizer.ID_ROOT) { return; } 166 ZmFolderTree._traverse(folder, obj, tree, (path || []), elementType, account); 167 } 168 } 169 170 return folder; 171 }; 172 173 ZmFolderTree.createAllDeferredFolders = 174 function() { 175 var ac = appCtxt.getAppController(); 176 for (var appId in ZmApp.ORGANIZER) { 177 var app = ac.getApp(appId); 178 app.createDeferred(); 179 } 180 }; 181 182 /** 183 * @private 184 */ 185 ZmFolderTree._traverse = 186 function(folder, obj, tree, path, elementType, account) { 187 188 var isRoot = (folder.nId == ZmOrganizer.ID_ROOT); 189 if (obj.folder && obj.folder.length) { 190 if (!isRoot) { 191 path.push(obj.name); 192 } 193 for (var i = 0; i < obj.folder.length; i++) { 194 var folderObj = obj.folder[i]; 195 var childFolder = ZmFolderTree.createFromJs(folder, folderObj, tree, (elementType || "folder"), path, account); 196 if (folder && childFolder) { 197 folder.children.add(childFolder); 198 } 199 } 200 if (!isRoot) { 201 path.pop(); 202 } 203 } 204 205 if (obj.search && obj.search.length) { 206 if (!isRoot) { 207 path.push(obj.name); 208 } 209 for (var i = 0; i < obj.search.length; i++) { 210 var searchObj = obj.search[i]; 211 var childSearch = ZmFolderTree.createFromJs(folder, searchObj, tree, "search", path, account); 212 if (childSearch) { 213 folder.children.add(childSearch); 214 } 215 } 216 if (!isRoot) { 217 path.pop(); 218 } 219 } 220 221 if (obj.link && obj.link.length) { 222 for (var i = 0; i < obj.link.length; i++) { 223 var link = obj.link[i]; 224 var childFolder = ZmFolderTree.createFromJs(folder, link, tree, "link", path, account); 225 if (childFolder) { 226 folder.children.add(childFolder); 227 } 228 } 229 } 230 }; 231 232 /** 233 * Creates the folder. 234 * 235 * @param {String} type the folder type 236 * @param {ZmFolder} parent the parent folder 237 * @param {Object} obj the JSON with folder data 238 * @param {ZmFolderTree} tree the containing tree 239 * @param {Array} path the list of path elements 240 * @param {String} elementType the type of containing JSON element 241 * @param {ZmZimbraAccount} account the account this folder belongs to 242 */ 243 ZmFolderTree.createFolder = 244 function(type, parent, obj, tree, path, elementType, account) { 245 var orgClass = eval(ZmOrganizer.ORG_CLASS[type]); 246 if (!orgClass) { return null; } 247 248 DBG.println(AjxDebug.DBG2, "Creating " + type + " with id " + obj.id + " and name " + obj.name); 249 250 var params = { 251 id: obj.id, 252 name: obj.name, 253 parent: parent, 254 tree: tree, 255 color: obj.color, 256 rgb: obj.rgb, 257 owner: obj.owner, 258 oname: obj.oname, 259 zid: obj.zid, 260 rid: obj.rid, 261 restUrl: obj.rest, 262 url: obj.url, 263 numUnread: obj.u, 264 numTotal: obj.n, 265 sizeTotal: obj.s, 266 perm: obj.perm, 267 link: elementType == "link", 268 broken: obj.broken, 269 account: account, 270 webOfflineSyncDays : obj.webOfflineSyncDays, 271 retentionPolicy: obj.retentionPolicy 272 }; 273 274 var folder = new orgClass(params); 275 ZmFolderTree._fillInFolder(folder, obj, path); 276 ZmFolderTree.IS_PARSED[type] = true; 277 278 return folder; 279 }; 280 281 /** 282 * @private 283 */ 284 ZmFolderTree._fillInFolder = 285 function(folder, obj, path) { 286 if (path && path.length) { 287 folder.path = path.join("/"); 288 } 289 290 if (obj.f && folder._parseFlags) { 291 folder._parseFlags(obj.f); 292 } 293 294 folder._setSharesFromJs(obj); 295 }; 296 297 /** 298 * Gets the folder by type. 299 * 300 * @param {String} type the type 301 * @return {ZmFolder} the folder or <code>null</code> if not found 302 */ 303 ZmFolderTree.prototype.getByType = 304 function(type) { 305 return this.root ? this.root.getByType(type) : null; 306 }; 307 308 /** 309 * Gets the folder by path. 310 * 311 * @param {String} path the path 312 * @param {Boolean} useSystemName <code>true</code> to use the system name 313 * @return {ZmFolder} the folder or <code>null</code> if not found 314 */ 315 ZmFolderTree.prototype.getByPath = 316 function(path, useSystemName) { 317 return this.root ? this.root.getByPath(path, useSystemName) : null; 318 }; 319 320 /** 321 * Handles a missing link by marking its organizer as not there, redrawing it in 322 * any tree views, and asking to delete it. 323 * 324 * @param {int} organizerType the type of organizer (constants defined in {@link ZmOrganizer}) 325 * @param {String} zid the zid of the missing folder 326 * @param {String} rid the rid of the missing folder 327 * @return {Boolean} <code>true</code> if the error is handled 328 */ 329 ZmFolderTree.prototype.handleNoSuchFolderError = 330 function(organizerType, zid, rid) { 331 var items = this.getByType(organizerType); 332 333 var treeView; 334 var handled = false; 335 if (items) { 336 for (var i = 0; i < items.length; i++) { 337 if ((items[i].zid == zid) && (items[i].rid == rid)) { 338 // Mark that the item is not there any more. 339 items[i].noSuchFolder = true; 340 341 // Change its appearance in the tree. 342 if (!treeView) { 343 var overviewId = appCtxt.getAppController().getOverviewId(); 344 treeView = appCtxt.getOverviewController().getTreeView(overviewId, organizerType); 345 } 346 var node = treeView.getTreeItemById(items[i].id); 347 node.setText(items[i].getName(true)); 348 349 // Ask if it should be deleted now. 350 this.handleDeleteNoSuchFolder(items[i]); 351 handled = true; 352 } 353 } 354 } 355 return handled; 356 }; 357 358 /** 359 * Handles no such folder. The user will be notified that a linked organizer generated a "no such folder", 360 * error, giving the user a chance to delete the folder. 361 * 362 * @param {ZmOrganizer} organizer the organizer 363 */ 364 ZmFolderTree.prototype.handleDeleteNoSuchFolder = 365 function(organizer) { 366 var ds = appCtxt.getYesNoMsgDialog(); 367 ds.reset(); 368 ds.registerCallback(DwtDialog.YES_BUTTON, this._deleteOrganizerYesCallback, this, [organizer, ds]); 369 ds.registerCallback(DwtDialog.NO_BUTTON, appCtxt.getAppController()._clearDialog, this, ds); 370 var msg = AjxMessageFormat.format(ZmMsg.confirmDeleteMissingFolder, AjxStringUtil.htmlEncode(organizer.getName(false, 0, true))); 371 ds.setMessage(msg, DwtMessageDialog.WARNING_STYLE); 372 ds.popup(); 373 }; 374 375 /** 376 * Handles the "Yes" button in the delete organizer dialog. 377 * 378 * @param {ZmOrganizer} organizer the organizer 379 * @param {ZmDialog} dialog the dialog 380 */ 381 ZmFolderTree.prototype._deleteOrganizerYesCallback = 382 function(organizer, dialog) { 383 organizer._delete(); 384 appCtxt.getAppController()._clearDialog(dialog); 385 }; 386 387 /** 388 * Issues a <code><BatchRequest></code> of <code><GetFolderRequest></code>s for existing 389 * mountpoints that do not have permissions set. 390 * 391 * @param {Hash} params a hash of parameters 392 * @param {int} params.type the {@link ZmItem} type constant 393 * @param {AjxCallback} params.callback the callback to trigger after fetching permissions 394 * @param {Boolean} params.skipNotify <code>true</code> to skip notify after fetching permissions 395 * @param {Array} params.folderIds the list of folder Id's to fetch permissions for 396 * @param {Boolean} params.noBusyOverlay <code>true</code> to not block the UI while fetching permissions 397 * @param {String} params.accountName the account to issue request under 398 */ 399 ZmFolderTree.prototype.getPermissions = 400 function(params) { 401 var needPerms = params.folderIds || this._getItemsWithoutPerms(params.type); 402 403 // build batch request to get all permissions at once 404 if (needPerms.length > 0) { 405 var soapDoc = AjxSoapDoc.create("BatchRequest", "urn:zimbra"); 406 soapDoc.setMethodAttribute("onerror", "continue"); 407 408 var doc = soapDoc.getDoc(); 409 for (var j = 0; j < needPerms.length; j++) { 410 var folderRequest = soapDoc.set("GetFolderRequest", null, null, "urn:zimbraMail"); 411 var folderNode = doc.createElement("folder"); 412 folderNode.setAttribute("l", needPerms[j]); 413 folderRequest.appendChild(folderNode); 414 } 415 416 var respCallback = new AjxCallback(this, this._handleResponseGetShares, [params.callback, params.skipNotify]); 417 appCtxt.getRequestMgr().sendRequest({ 418 soapDoc: soapDoc, 419 asyncMode: true, 420 callback: respCallback, 421 noBusyOverlay: params.noBusyOverlay, 422 accountName: params.accountName 423 }); 424 } else { 425 if (params.callback) { 426 params.callback.run(); 427 } 428 } 429 }; 430 431 /** 432 * @private 433 */ 434 ZmFolderTree.prototype._getItemsWithoutPerms = 435 function(type) { 436 var needPerms = []; 437 var orgs = type ? [type] : [ZmOrganizer.FOLDER, ZmOrganizer.CALENDAR, ZmOrganizer.TASKS, ZmOrganizer.BRIEFCASE, ZmOrganizer.ADDRBOOK]; 438 439 for (var j = 0; j < orgs.length; j++) { 440 var org = orgs[j]; 441 if (!ZmFolderTree.IS_PARSED[org]) { continue; } 442 443 var items = this.getByType(org); 444 445 for (var i = 0; i < items.length; i++) { 446 if (items[i].link && items[i].shares == null) { 447 needPerms.push(items[i].id); 448 } 449 } 450 } 451 452 return needPerms; 453 }; 454 455 /** 456 * @private 457 */ 458 ZmFolderTree.prototype._handleResponseGetShares = 459 function(callback, skipNotify, result) { 460 var batchResp = result.getResponse().BatchResponse; 461 this._handleErrorGetShares(batchResp); 462 463 var resp = batchResp.GetFolderResponse; 464 if (resp) { 465 for (var i = 0; i < resp.length; i++) { 466 var link = resp[i].link ? resp[i].link[0] : null; 467 if (link) { 468 var mtpt = appCtxt.getById(link.id); 469 if (mtpt) { 470 // update the mtpt perms with the updated link perms 471 mtpt.perm = link.perm; 472 if (link.n) mtpt.numTotal=link.n; 473 if (link.u) mtpt.numUnread=link.u; 474 mtpt._setSharesFromJs(link); 475 } 476 477 if (link.folder && link.folder.length > 0) { 478 var parent = appCtxt.getById(link.id); 479 if (parent) { 480 // TODO: only goes one level deep - should we recurse? 481 for (var j = 0; j < link.folder.length; j++) { 482 if (appCtxt.getById(link.folder[j].id)) { continue; } 483 parent.notifyCreate(link.folder[j], "link", skipNotify); 484 } 485 } 486 } 487 } 488 } 489 } 490 491 if (callback) { 492 callback.run(); 493 } 494 }; 495 496 /** 497 * Handles errors that come back from the GetShares batch request. 498 * 499 * @param {Array} organizerTypes the types of organizer (constants defined in {@link ZmOrganizer}) 500 * @param {Object} batchResp the response 501 * 502 */ 503 ZmFolderTree.prototype._handleErrorGetShares = 504 function(batchResp) { 505 var faults = batchResp.Fault; 506 if (faults) { 507 var rids = []; 508 var zids = []; 509 for (var i = 0, length = faults.length; i < length; i++) { 510 var ex = ZmCsfeCommand.faultToEx(faults[i]); 511 if (ex.code == ZmCsfeException.MAIL_NO_SUCH_FOLDER) { 512 var itemId = ex.data.itemId[0]; 513 var index = itemId.lastIndexOf(':'); 514 zids.push(itemId.substring(0, index)); 515 rids.push(itemId.substring(index + 1, itemId.length)); 516 } 517 } 518 if (zids.length) { 519 this._markNoSuchFolder(zids, rids); 520 } 521 } 522 }; 523 524 /** 525 * Handles missing links by marking the organizers as not there 526 * 527 * @param {Array} zids the zids of the missing folders 528 * @param {Array} rids the rids of the missing folders. rids and zids must have the same length 529 * 530 */ 531 ZmFolderTree.prototype._markNoSuchFolder = 532 function(zids, rids) { 533 var treeData = appCtxt.getFolderTree(); 534 var items = treeData && treeData.root 535 ? treeData.root.children.getArray() 536 : null; 537 538 for (var i = 0; i < items.length; i++) { 539 for (var j = 0; j < rids.length; j++) { 540 if ((items[i].zid == zids[j]) && (items[i].rid == rids[j])) { 541 items[i].noSuchFolder = true; 542 } 543 } 544 } 545 }; 546 547 /** 548 * @private 549 */ 550 ZmFolderTree.prototype._sortFolder = 551 function(folder) { 552 var children = folder.children; 553 if (children && children.length) { 554 children.sort(ZmFolder.sortCompare); 555 for (var i = 0; i < children.length; i++) 556 this._sortFolder(children[i]); 557 } 558 };