Index: src/wp-includes/js/heartbeat.js
===================================================================
--- src/wp-includes/js/heartbeat.js	(revision 29774)
+++ src/wp-includes/js/heartbeat.js	(working copy)
@@ -57,6 +57,9 @@
 				// Used when the interval is reset
 				originalInterval: 0,
 
+				// Used to limit the number of AJAX requests.
+				minimalInterval: 0,
+
 				// Used together with tempInterval
 				countdown: 0,
 
@@ -81,10 +84,7 @@
 				// Flags whether events tracking user activity were set
 				userActivityEvents: false,
 
-				// References to various timeouts
-				beatTimer: 0,
-				winBlurTimer: 0,
-				frameBlurTimer: 0
+				beatTimer: 0
 			};
 
 		/**
@@ -95,6 +95,8 @@
 		 * @return void
 		 */
 		function initialize() {
+			var options, hidden, visibilityState, visibilitychange;
+
 			if ( typeof window.pagenow === 'string' ) {
 				settings.screenId = window.pagenow;
 			}
@@ -105,7 +107,7 @@
 
 			// Pull in options passed from PHP
 			if ( typeof window.heartbeatSettings === 'object' ) {
-				var options = window.heartbeatSettings;
+				options = window.heartbeatSettings;
 
 				// The XHR URL can be passed as option when window.ajaxurl is not set
 				if ( ! settings.url && options.ajaxurl ) {
@@ -112,17 +114,32 @@
 					settings.url = options.ajaxurl;
 				}
 
-				// The interval can be from 15 to 60 sec. and can be set temporarily to 5 sec.
+				// The interval can be from 15 to 120 sec. and can be set temporarily to 5 sec.
+				// It can be set in the initial options or changed later from JS and/or from PHP.
 				if ( options.interval ) {
 					settings.mainInterval = options.interval;
 
 					if ( settings.mainInterval < 15 ) {
 						settings.mainInterval = 15;
-					} else if ( settings.mainInterval > 60 ) {
-						settings.mainInterval = 60;
+					} else if ( settings.mainInterval > 120 ) {
+						settings.mainInterval = 120;
 					}
 				}
 
+				// Used to limit the number of AJAX requests. Overrides all other intervals if they are shorter.
+				// Needed for some hosts that cannot handle frequent requests and the user may exceed the allocated server CPU time, etc.
+				// The minimal interval can be up to 600 sec. however setting it to longer than 120 sec. will limit or disable
+				// some of the functionality (like post locks).
+				// Once set at initialization, minimalInterval cannot be changed/overriden.
+				if ( options.minimalInterval ) {
+					options.minimalInterval = parseInt( options.minimalInterval, 10 );
+					settings.minimalInterval = options.minimalInterval > 0 && options.minimalInterval <= 600 ? options.minimalInterval * 1000 : 0;
+				}
+
+				if ( settings.minimalInterval && settings.mainInterval < settings.minimalInterval ) {
+					settings.mainInterval = settings.minimalInterval;
+				}
+
 				// 'screenId' can be added from settings on the front-end where the JS global 'pagenow' is not set
 				if ( ! settings.screenId ) {
 					settings.screenId = options.screenId || 'front';
@@ -137,16 +154,38 @@
 			settings.mainInterval = settings.mainInterval * 1000;
 			settings.originalInterval = settings.mainInterval;
 
-			// Set focus/blur events on the window
-			$(window).on( 'blur.wp-heartbeat-focus', function() {
-				setFrameFocusEvents();
-				// We don't know why the 'blur' was fired. Either the user clicked in an iframe or outside the browser.
-				// Running blurred() after some timeout lets us cancel it if the user clicked in an iframe.
-				settings.winBlurTimer = window.setTimeout( function(){ blurred(); }, 500 );
-			}).on( 'focus.wp-heartbeat-focus', function() {
-				removeFrameFocusEvents();
-				focused();
-			}).on( 'unload.wp-heartbeat', function() {
+			// Switch the interval to 120 sec. by using the Page Visibility API.
+			// If the browser doesn't support it (Safari < 7, Android < 4.4, IE < 10), the interval
+			// will be increased to 120 sec. after 5 min. of mouse and keyboard inactivity.
+			if ( typeof document.hidden !== 'undefined' ) {
+				hidden = 'hidden';
+				visibilitychange = 'visibilitychange';
+				visibilityState = 'visibilityState';
+			} else if ( typeof document.msHidden !== 'undefined' ) { // IE10
+				hidden = 'msHidden';
+				visibilitychange = 'msvisibilitychange';
+				visibilityState = 'msVisibilityState';
+			} else if ( typeof document.webkitHidden !== 'undefined' ) { // Android
+				hidden = 'webkitHidden';
+				visibilitychange = 'webkitvisibilitychange';
+				visibilityState = 'webkitVisibilityState';
+			}
+
+			if ( hidden ) {
+				if ( document[hidden] ) {
+					settings.hasFocus = false;
+				}
+
+				$document.on( visibilitychange + '.wp-heartbeat', function() {
+					if ( document[visibilityState] === 'hidden' ) {
+						blurred();
+					} else {
+						focused();
+					}
+				});
+			}
+
+			$(window).on( 'unload.wp-heartbeat', function() {
 				// Don't connect any more
 				settings.suspend = true;
 
@@ -374,6 +413,10 @@
 				}
 			}
 
+			if ( settings.minimalInterval && interval < settings.minimalInterval ) {
+				interval = settings.minimalInterval;
+			}
+
 			window.clearTimeout( settings.beatTimer );
 
 			if ( delta < interval ) {
@@ -389,7 +432,7 @@
 		}
 
 		/**
-		 * Set the internal state when the browser window looses focus
+		 * Set the internal state when the browser window is hidden
 		 *
 		 * @access private
 		 *
@@ -396,12 +439,11 @@
 		 * @return void
 		 */
 		function blurred() {
-			clearFocusTimers();
 			settings.hasFocus = false;
 		}
 
 		/**
-		 * Set the internal state when the browser window is focused
+		 * Set the internal state when the browser window is visible
 		 *
 		 * @access private
 		 *
@@ -408,7 +450,6 @@
 		 * @return void
 		 */
 		function focused() {
-			clearFocusTimers();
 			settings.userActivity = time();
 
 			// Resume if suspended
@@ -421,68 +462,6 @@
 		}
 
 		/**
-		 * Add focus/blur events to all local iframes
-		 *
-		 * Used to detect when focus is moved from the main window to an iframe
-		 *
-		 * @access private
-		 *
-		 * @return void
-		 */
-		function setFrameFocusEvents() {
-			$('iframe').each( function( i, frame ) {
-				if ( ! isLocalFrame( frame ) ) {
-					return;
-				}
-
-				if ( $.data( frame, 'wp-heartbeat-focus' ) ) {
-					return;
-				}
-
-				$.data( frame, 'wp-heartbeat-focus', 1 );
-
-				$( frame.contentWindow ).on( 'focus.wp-heartbeat-focus', function() {
-					focused();
-				}).on('blur.wp-heartbeat-focus', function() {
-					setFrameFocusEvents();
-					// We don't know why 'blur' was fired. Either the user clicked in the main window or outside the browser.
-					// Running blurred() after some timeout lets us cancel it if the user clicked in the main window.
-					settings.frameBlurTimer = window.setTimeout( function(){ blurred(); }, 500 );
-				});
-			});
-		}
-
-		/**
-		 * Remove the focus/blur events to all local iframes
-		 *
-		 * @access private
-		 *
-		 * @return void
-		 */
-		function removeFrameFocusEvents() {
-			$('iframe').each( function( i, frame ) {
-				if ( ! isLocalFrame( frame ) ) {
-					return;
-				}
-
-				$.removeData( frame, 'wp-heartbeat-focus' );
-				$( frame.contentWindow ).off( '.wp-heartbeat-focus' );
-			});
-		}
-
-		/**
-		 * Clear the reset timers for focus/blur events on the window and iframes
-		 *
-		 * @access private
-		 *
-		 * @return void
-		 */
-		function clearFocusTimers() {
-			window.clearTimeout( settings.winBlurTimer );
-			window.clearTimeout( settings.frameBlurTimer );
-		}
-
-		/**
 		 * Runs when the user becomes active after a period of inactivity
 		 *
 		 * @access private
@@ -524,9 +503,12 @@
 				blurred();
 			}
 
-			if ( settings.suspendEnabled && lastActive > 1200000 ) {
-				// Suspend after 20 min. of inactivity
+			if ( settings.suspendEnabled && lastActive > 600000 ) {
+				// Suspend after 10 min. of inactivity when suspending is enabled.
 				settings.suspend = true;
+			} else if ( lastActive > 3600000 ) {
+				// Always suspend after 60 min. of inactivity. This will release the post lock, etc.
+				settings.suspend = true;
 			}
 
 			if ( ! settings.userActivityEvents ) {
@@ -597,7 +579,7 @@
 		 * In this case the number of 'ticks' can be passed as second argument.
 		 * If the window doesn't have focus, the interval slows down to 2 min.
 		 *
-		 * @param mixed speed Interval: 'fast' or 5, 15, 30, 60
+		 * @param mixed speed Interval: 'fast' or 5, 15, 30, 60, 120
 		 * @param string ticks Used with speed = 'fast' or 5, how many ticks before the interval reverts back
 		 * @return int Current interval in seconds
 		 */
@@ -620,6 +602,9 @@
 					case 60:
 						newInterval = 60000;
 						break;
+					case 120:
+						newInterval = 120000;
+						break;
 					case 'long-polling':
 						// Allow long polling, (experimental)
 						settings.mainInterval = 0;
@@ -628,6 +613,10 @@
 						newInterval = settings.originalInterval;
 				}
 
+				if ( settings.minimalInterval && newInterval < settings.minimalInterval ) {
+					newInterval = settings.minimalInterval;
+				}
+
 				if ( 5000 === newInterval ) {
 					ticks = parseInt( ticks, 10 ) || 30;
 					ticks = ticks < 1 || ticks > 30 ? 30 : ticks;
