1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2007, 2009, 2010, 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) 2007, 2009, 2010, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 /*
 24    Derived from "Really Simple History", by Brad Neuberg. Its copyright follows:
 25 
 26    Copyright (c) 2005, Brad Neuberg, bkn3@columbia.edu
 27    http://codinginparadise.org
 28    
 29    Permission is hereby granted, free of charge, to any person obtaining 
 30    a copy of this software and associated documentation files (the "Software"), 
 31    to deal in the Software without restriction, including without limitation 
 32    the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 33    and/or sell copies of the Software, and to permit persons to whom the 
 34    Software is furnished to do so, subject to the following conditions:
 35    
 36    The above copyright notice and this permission notice shall be 
 37    included in all copies or substantial portions of the Software.
 38    
 39    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
 40    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
 41    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 42    IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
 43    CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT 
 44    OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 
 45    THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 46  */
 47 
 48 /**
 49  * Initializes history support.
 50  * @constructor
 51  * @class
 52  * This singleton class provides support for handling history changes via the Back
 53  * and Forward buttons. Since an Ajax application represents a single URL, hitting
 54  * the Back button will ordinarily unload the app, which is usually not what the user
 55  * wants to do. Changing the hash value in the browser's location will affect the
 56  * browser history, but not the content. IE also uses a hidden iframe to track history.
 57  * <p>
 58  * The code below is a stripped-down version of Brad Neuberg's Really Simple History
 59  * (see copyright above). Support for history storage has been removed.</p>
 60  * 
 61  * @author Conrad Damon
 62  * 
 63  * TODO: - add enable()
 64  * 
 65  * @private
 66  */
 67 AjxHistoryMgr = function() {
 68 
 69 	this.currentLocation = null;			// Our current hash location, without the "#" symbol.
 70 	this.listener = null;					// Our history change listener. */
 71 	this.iframe = null;						// A hidden IFrame we use in Internet Explorer to detect history changes.
 72 	this.ignoreLocationChange = null;		// Indicates to the browser whether to ignore location changes.
 73 
 74 	// The amount of time in ms to wait between add requests. 
 75 	this.WAIT_TIME = AjxEnv.isIE ? 400 : 200;
 76 
 77 	this.currentWaitTime = 0;				// Time in ms to wait before calling setTimeout to add a location
 78 
 79 	/** A variable to handle an important edge case in Internet
 80 	Explorer. In IE, if a user manually types an address into
 81 	their browser's location bar, we must intercept this by
 82 	continuously checking the location bar with a timer 
 83 	interval. However, if we manually change the location
 84 	bar ourselves programmatically, when using our hidden
 85 	iframe, we need to ignore these changes. Unfortunately,
 86 	these changes are not atomic, so we surround them with
 87 	the variable 'ieAtomicLocationChange', that if true,
 88 	means we are programmatically setting the location and
 89 	should ignore this atomic chunked change. */
 90 	this.ieAtomicLocationChange = null;
 91 
 92 	this._eventMgr = new AjxEventMgr();
 93 	this._evt = new AjxEvent();
 94 
 95 	this._initialize();
 96 }
 97 
 98 // Name of the file that has content for the iframe
 99 AjxHistoryMgr.BLANK_FILE = "blankHistory.html";
100 
101 // ID for the iframe
102 AjxHistoryMgr.IFRAME_ID = "DhtmlHistoryFrame";
103 
104 
105 /**
106  * Adds a history change listener.
107  */
108 AjxHistoryMgr.prototype.addListener =
109 function(listener) {
110 	return this._eventMgr.addListener(AjxEvent.HISTORY, listener);
111 };
112 
113 /**
114  * Removes a history change listener.
115  */
116 AjxHistoryMgr.prototype.removeListener =
117 function(listener) {
118 	return this._eventMgr.removeListener(AjxEvent.HISTORY, listener);
119 };
120 
121 /**
122  * Most browsers require that we wait a certain amount of time before changing the
123  * location, such as 200 milliseconds; rather than forcing external callers to use
124  * window.setTimeout to account for this to prevent bugs, we internally handle this
125  * detail by using a 'currentWaitTime' variable and have requests wait in line
126  */
127 AjxHistoryMgr.prototype.add =
128 function(newLocation) {
129 
130 	var self = this;
131 	var addImpl = function() {
132 		
133 		// indicate that the current wait time is now less
134 		if (self.currentWaitTime > 0) {
135 			self.currentWaitTime = self.currentWaitTime - self.WAIT_TIME;
136 		}
137 		
138 		// remove any leading hash symbols on newLocation
139 		newLocation = self._removeHash(newLocation);
140 		   
141 		// IE has a strange bug; if the newLocation
142 		// is the same as _any_ preexisting id in the
143 		// document, then the history action gets recorded
144 		// twice; throw a programmer exception if there is
145 		// an element with this ID
146 		if (AjxEnv.isIE) {
147 			if (document.getElementById(newLocation)) {
148 				throw new DwtException("AjxHistoryMgr: location has same ID as DOM element");
149 			}
150 		}
151 
152 		// indicate to the browser to ignore this upcoming location change
153 		self.ignoreLocationChange = true;
154 		 
155 		// indicate to IE that this is an atomic location change block
156 		this.ieAtomicLocationChange = true;
157 		     
158 		// save this as our current location
159 		self.currentLocation = newLocation;
160 		   
161 		// change the browser location
162 		window.location.hash = newLocation;
163 		   
164 		// change the hidden iframe's location if on IE
165 		if (AjxEnv.isIE) {
166 			self.iframe.src = AjxHistoryMgr.BLANK_FILE + "?" + newLocation;
167 		}
168 		
169 		// end of atomic location change block for IE
170 		this.ieAtomicLocationChange = false;
171 	};		
172 		
173 	// queue up requests
174 	window.setTimeout(addImpl, this.currentWaitTime);
175 	   
176 	// indicate that the next request will have to wait for awhile
177 	this.currentWaitTime = this.currentWaitTime + self.WAIT_TIME;
178 };
179 
180 /**
181  * Returns the current hash value that is in the browser's
182  * location bar, removing leading # symbols if they are present.
183  */   
184 AjxHistoryMgr.prototype.getCurrentLocation =
185 function() {
186 	return this._removeHash(window.location.hash);
187 };
188 
189 /**
190  * Creates the DHTML history infrastructure.
191  */
192 AjxHistoryMgr.prototype._initialize =
193 function() {
194 
195 	// get our initial location
196 	var initialHash = this.getCurrentLocation();
197 	
198 	// save this as our current location
199 	this.currentLocation = initialHash;
200 	
201 	// write out a hidden iframe for IE and
202 	// set the amount of time to wait between add() requests
203 	if (AjxEnv.isIE) {
204 		DBG.println(AjxDebug.DBG2, "Creating iframe for IE: " + AjxHistoryMgr.BLANK_FILE);
205 		var html = [];
206 		var i = 0;
207 		html[i++] = "<iframe style='border: 0px; width: 1px; ";
208 		html[i++] = "height: 1px; position: absolute; bottom: 0px; ";
209 		html[i++] = "right: 0px; visibility: visible;' ";
210 		html[i++] = "id='" + AjxHistoryMgr.IFRAME_ID + "' ";
211 		html[i++] = "src='" + AjxHistoryMgr.BLANK_FILE + "?" + initialHash + "'>";
212 		html[i++] = "</iframe>";
213 		
214 		var htmlElement = document.createElement("div");
215 		document.body.appendChild(htmlElement);
216 		htmlElement.innerHTML = html.join("");
217 	}
218 
219 	if (AjxEnv.isIE) {
220 		this.iframe = document.getElementById(AjxHistoryMgr.IFRAME_ID);
221 	}  
222 
223 	// other browsers can use a location handler that checks
224 	// at regular intervals as their primary mechanism;
225 	// we use it for Internet Explorer as well to handle
226 	// an important edge case; see _checkLocation() for
227 	// details
228 	var self = this;
229 	var locationHandler = function() {
230 		self._checkLocation();
231 	};
232 	window.onhashchange = locationHandler;
233 };
234 
235 /**
236  * Checks if the browsers has changed location.  This is the primary history mechanism
237  * for Firefox. For Internet Explorer, we use this to handle an important edge case:
238  * if a user manually types in a new hash value into their Internet Explorer location
239  * bar and press enter, we want to intercept this and notify any history listener.
240  */
241 AjxHistoryMgr.prototype._checkLocation =
242 function() {
243 	// ignore any location changes that we made ourselves
244 	// for browsers other than Internet Explorer
245 	if (!AjxEnv.isIE && this.ignoreLocationChange) {
246 	   this.ignoreLocationChange = false;
247 	   return;
248 	}
249 
250 	// if we are dealing with Internet Explorer
251 	// and we are in the middle of making a location
252 	// change from an iframe, ignore it
253 	if (!AjxEnv.isIE && this.ieAtomicLocationChange) {
254 	   return;
255 	}
256 
257 	// get hash location
258 	var hash = this.getCurrentLocation();
259 
260 	// see if there has been a change
261 	if (hash == this.currentLocation) { return; }
262    
263 	// on Internet Explorer, we need to intercept users manually
264 	// entering locations into the browser; we do this by comparing
265 	// the browsers location against the iframes location; if they
266 	// differ, we are dealing with a manual event and need to
267 	// place it inside our history, otherwise we can return
268 	this.ieAtomicLocationChange = true;
269 
270 	if (AjxEnv.isIE && this._getIFrameHash() != hash) {
271 	   this.iframe.src = AjxHistoryMgr.BLANK_FILE + "?" + hash;
272 	} else if (AjxEnv.isIE) {
273 	   // the iframe is unchanged
274 	   return;
275 	}
276    
277 	// save this new location
278 	this.currentLocation = hash;
279 	
280 	this.ieAtomicLocationChange = false;
281 	
282 	// notify listeners of the change
283 	this._evt.data = hash;
284 	this._eventMgr.notifyListeners(AjxEvent.HISTORY, this._evt);
285 };
286 
287 /**
288  * Gets the current location of the hidden IFrames
289  * that is stored as history. For Internet Explorer.
290  */
291 AjxHistoryMgr.prototype._getIFrameHash =
292 function() {
293 	// get the new location
294 	var historyFrame = document.getElementById(AjxHistoryMgr.IFRAME_ID);
295 	var doc = historyFrame.contentWindow.document;
296 	var hash = new String(doc.location.search);
297 	
298 	if (hash.length == 1 && hash.charAt(0) == "?") {
299 		hash = "";
300 	} else if (hash.length >= 2 && hash.charAt(0) == "?") {
301 		hash = hash.substring(1);
302 	}
303     
304 	return hash;
305 };
306    
307 /**
308  * Removes any leading hash that might be on a location.
309  */
310 AjxHistoryMgr.prototype._removeHash =
311 function(hashValue) {
312 	if (hashValue == null || hashValue == undefined) {
313 	   return null;
314 	} else if (hashValue == "") {
315 	   return "";
316 	} else if (hashValue.length == 1 && hashValue.charAt(0) == "#") {
317 	   return "";
318 	} else if (hashValue.length > 1 && hashValue.charAt(0) == "#") {
319 	   return hashValue.substring(1);
320 	} else {
321 	   return hashValue;
322 	}
323 };
324    
325 /**
326  * For IE, says when the hidden iframe has finished loading.
327  */
328 AjxHistoryMgr.prototype.iframeLoaded =
329 function(newLocation) {
330 
331 	// ignore any location changes that we made ourselves
332 	if (this.ignoreLocationChange) {
333 	   this.ignoreLocationChange = false;
334 	   return;
335 	}
336 	
337 	// get the new location
338 	var hash = new String(newLocation.search);
339 	if (hash.length == 1 && hash.charAt(0) == "?") {
340 		hash = "";
341 	} else if (hash.length >= 2 && hash.charAt(0) == "?") {
342 		hash = hash.substring(1);
343 	}
344 	
345 	// move to this location in the browser location bar
346 	window.location.hash = hash;
347 	
348 	// notify listeners of the change
349 	this._evt.data = hash;
350 	this._eventMgr.notifyListeners(AjxEvent.HISTORY, this._evt);
351 };
352