]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/heartbeat.js
WordPress 3.9-scripts
[autoinstalls/wordpress.git] / wp-includes / js / heartbeat.js
1 /**
2  * Heartbeat API
3  *
4  * Heartbeat is a simple server polling API that sends XHR requests to
5  * the server every 15 - 60 seconds and triggers events (or callbacks) upon
6  * receiving data. Currently these 'ticks' handle transports for post locking,
7  * login-expiration warnings, autosave, and related tasks while a user is logged in.
8  *
9  * Available PHP filters (in ajax-actions.php):
10  * - heartbeat_received
11  * - heartbeat_send
12  * - heartbeat_tick
13  * - heartbeat_nopriv_received
14  * - heartbeat_nopriv_send
15  * - heartbeat_nopriv_tick
16  * @see wp_ajax_nopriv_heartbeat(), wp_ajax_heartbeat()
17  *
18  * Custom jQuery events:
19  * - heartbeat-send
20  * - heartbeat-tick
21  * - heartbeat-error
22  * - heartbeat-connection-lost
23  * - heartbeat-connection-restored
24  * - heartbeat-nonces-expired
25  *
26  * @since 3.6.0
27  */
28
29 ( function( $, window, undefined ) {
30         var Heartbeat = function() {
31                 var $document = $(document),
32                         settings = {
33                                 // Suspend/resume
34                                 suspend: false,
35
36                                 // Whether suspending is enabled
37                                 suspendEnabled: true,
38
39                                 // Current screen id, defaults to the JS global 'pagenow' when present (in the admin) or 'front'
40                                 screenId: '',
41
42                                 // XHR request URL, defaults to the JS global 'ajaxurl' when present
43                                 url: '',
44
45                                 // Timestamp, start of the last connection request
46                                 lastTick: 0,
47
48                                 // Container for the enqueued items
49                                 queue: {},
50
51                                 // Connect interval (in seconds)
52                                 mainInterval: 60,
53
54                                 // Used when the interval is set to 5 sec. temporarily
55                                 tempInterval: 0,
56
57                                 // Used when the interval is reset
58                                 originalInterval: 0,
59
60                                 // Used together with tempInterval
61                                 countdown: 0,
62
63                                 // Whether a connection is currently in progress
64                                 connecting: false,
65
66                                 // Whether a connection error occured
67                                 connectionError: false,
68
69                                 // Used to track non-critical errors
70                                 errorcount: 0,
71
72                                 // Whether at least one connection has completed successfully
73                                 hasConnected: false,
74
75                                 // Whether the current browser window is in focus and the user is active
76                                 hasFocus: true,
77
78                                 // Timestamp, last time the user was active. Checked every 30 sec.
79                                 userActivity: 0,
80
81                                 // Flags whether events tracking user activity were set
82                                 userActivityEvents: false,
83
84                                 // References to various timeouts
85                                 beatTimer: 0,
86                                 winBlurTimer: 0,
87                                 frameBlurTimer: 0
88                         };
89
90                 /**
91                  * Set local vars and events, then start
92                  *
93                  * @access private
94                  *
95                  * @return void
96                  */
97                 function initialize() {
98                         if ( typeof window.pagenow === 'string' ) {
99                                 settings.screenId = window.pagenow;
100                         }
101
102                         if ( typeof window.ajaxurl === 'string' ) {
103                                 settings.url = window.ajaxurl;
104                         }
105
106                         // Pull in options passed from PHP
107                         if ( typeof window.heartbeatSettings === 'object' ) {
108                                 var options = window.heartbeatSettings;
109
110                                 // The XHR URL can be passed as option when window.ajaxurl is not set
111                                 if ( ! settings.url && options.ajaxurl ) {
112                                         settings.url = options.ajaxurl;
113                                 }
114
115                                 // The interval can be from 15 to 60 sec. and can be set temporarily to 5 sec.
116                                 if ( options.interval ) {
117                                         settings.mainInterval = options.interval;
118
119                                         if ( settings.mainInterval < 15 ) {
120                                                 settings.mainInterval = 15;
121                                         } else if ( settings.mainInterval > 60 ) {
122                                                 settings.mainInterval = 60;
123                                         }
124                                 }
125
126                                 // 'screenId' can be added from settings on the front-end where the JS global 'pagenow' is not set
127                                 if ( ! settings.screenId ) {
128                                         settings.screenId = options.screenId || 'front';
129                                 }
130
131                                 if ( options.suspension === 'disable' ) {
132                                         settings.suspendEnabled = false;
133                                 }
134                         }
135
136                         // Convert to milliseconds
137                         settings.mainInterval = settings.mainInterval * 1000;
138                         settings.originalInterval = settings.mainInterval;
139
140                         // Set focus/blur events on the window
141                         $(window).on( 'blur.wp-heartbeat-focus', function() {
142                                 setFrameFocusEvents();
143                                 // We don't know why the 'blur' was fired. Either the user clicked in an iframe or outside the browser.
144                                 // Running blurred() after some timeout lets us cancel it if the user clicked in an iframe.
145                                 settings.winBlurTimer = window.setTimeout( function(){ blurred(); }, 500 );
146                         }).on( 'focus.wp-heartbeat-focus', function() {
147                                 removeFrameFocusEvents();
148                                 focused();
149                         }).on( 'unload.wp-heartbeat', function() {
150                                 // Don't connect any more
151                                 settings.suspend = true;
152
153                                 // Abort the last request if not completed
154                                 if ( settings.xhr && settings.xhr.readyState !== 4 ) {
155                                         settings.xhr.abort();
156                                 }
157                         });
158
159                         // Check for user activity every 30 seconds.
160                         window.setInterval( function(){ checkUserActivity(); }, 30000 );
161
162                         // Start one tick after DOM ready
163                         $document.ready( function() {
164                                 settings.lastTick = time();
165                                 scheduleNextTick();
166                         });
167                 }
168
169                 /**
170                  * Return the current time according to the browser
171                  *
172                  * @access private
173                  *
174                  * @return int
175                  */
176                 function time() {
177                         return (new Date()).getTime();
178                 }
179
180                 /**
181                  * Check if the iframe is from the same origin
182                  *
183                  * @access private
184                  *
185                  * @return bool
186                  */
187                 function isLocalFrame( frame ) {
188                         var origin, src = frame.src;
189
190                         // Need to compare strings as WebKit doesn't throw JS errors when iframes have different origin.
191                         // It throws uncatchable exceptions.
192                         if ( src && /^https?:\/\//.test( src ) ) {
193                                 origin = window.location.origin ? window.location.origin : window.location.protocol + '//' + window.location.host;
194
195                                 if ( src.indexOf( origin ) !== 0 ) {
196                                         return false;
197                                 }
198                         }
199
200                         try {
201                                 if ( frame.contentWindow.document ) {
202                                         return true;
203                                 }
204                         } catch(e) {}
205
206                         return false;
207                 }
208
209                 /**
210                  * Set error state and fire an event on XHR errors or timeout
211                  *
212                  * @access private
213                  *
214                  * @param string error The error type passed from the XHR
215                  * @param int status The HTTP status code passed from jqXHR (200, 404, 500, etc.)
216                  * @return void
217                  */
218                 function setErrorState( error, status ) {
219                         var trigger;
220
221                         if ( error ) {
222                                 switch ( error ) {
223                                         case 'abort':
224                                                 // do nothing
225                                                 break;
226                                         case 'timeout':
227                                                 // no response for 30 sec.
228                                                 trigger = true;
229                                                 break;
230                                         case 'error':
231                                                 if ( 503 === status && settings.hasConnected ) {
232                                                         trigger = true;
233                                                         break;
234                                                 }
235                                                 /* falls through */
236                                         case 'parsererror':
237                                         case 'empty':
238                                         case 'unknown':
239                                                 settings.errorcount++;
240
241                                                 if ( settings.errorcount > 2 && settings.hasConnected ) {
242                                                         trigger = true;
243                                                 }
244
245                                                 break;
246                                 }
247
248                                 if ( trigger && ! hasConnectionError() ) {
249                                         settings.connectionError = true;
250                                         $document.trigger( 'heartbeat-connection-lost', [error, status] );
251                                 }
252                         }
253                 }
254
255                 /**
256                  * Clear the error state and fire an event
257                  *
258                  * @access private
259                  *
260                  * @return void
261                  */
262                 function clearErrorState() {
263                         // Has connected successfully
264                         settings.hasConnected = true;
265
266                         if ( hasConnectionError() ) {
267                                 settings.errorcount = 0;
268                                 settings.connectionError = false;
269                                 $document.trigger( 'heartbeat-connection-restored' );
270                         }
271                 }
272
273                 /**
274                  * Gather the data and connect to the server
275                  *
276                  * @access private
277                  *
278                  * @return void
279                  */
280                 function connect() {
281                         var ajaxData, heartbeatData;
282
283                         // If the connection to the server is slower than the interval,
284                         // heartbeat connects as soon as the previous connection's response is received.
285                         if ( settings.connecting || settings.suspend ) {
286                                 return;
287                         }
288
289                         settings.lastTick = time();
290
291                         heartbeatData = $.extend( {}, settings.queue );
292                         // Clear the data queue, anything added after this point will be send on the next tick
293                         settings.queue = {};
294
295                         $document.trigger( 'heartbeat-send', [ heartbeatData ] );
296
297                         ajaxData = {
298                                 data: heartbeatData,
299                                 interval: settings.tempInterval ? settings.tempInterval / 1000 : settings.mainInterval / 1000,
300                                 _nonce: typeof window.heartbeatSettings === 'object' ? window.heartbeatSettings.nonce : '',
301                                 action: 'heartbeat',
302                                 screen_id: settings.screenId,
303                                 has_focus: settings.hasFocus
304                         };
305
306                         settings.connecting = true;
307                         settings.xhr = $.ajax({
308                                 url: settings.url,
309                                 type: 'post',
310                                 timeout: 30000, // throw an error if not completed after 30 sec.
311                                 data: ajaxData,
312                                 dataType: 'json'
313                         }).always( function() {
314                                 settings.connecting = false;
315                                 scheduleNextTick();
316                         }).done( function( response, textStatus, jqXHR ) {
317                                 var newInterval;
318
319                                 if ( ! response ) {
320                                         setErrorState( 'empty' );
321                                         return;
322                                 }
323
324                                 clearErrorState();
325
326                                 if ( response.nonces_expired ) {
327                                         $document.trigger( 'heartbeat-nonces-expired' );
328                                         return;
329                                 }
330
331                                 // Change the interval from PHP
332                                 if ( response.heartbeat_interval ) {
333                                         newInterval = response.heartbeat_interval;
334                                         delete response.heartbeat_interval;
335                                 }
336
337                                 $document.trigger( 'heartbeat-tick', [response, textStatus, jqXHR] );
338
339                                 // Do this last, can trigger the next XHR if connection time > 5 sec. and newInterval == 'fast'
340                                 if ( newInterval ) {
341                                         interval( newInterval );
342                                 }
343                         }).fail( function( jqXHR, textStatus, error ) {
344                                 setErrorState( textStatus || 'unknown', jqXHR.status );
345                                 $document.trigger( 'heartbeat-error', [jqXHR, textStatus, error] );
346                         });
347                 }
348
349                 /**
350                  * Schedule the next connection
351                  *
352                  * Fires immediately if the connection time is longer than the interval.
353                  *
354                  * @access private
355                  *
356                  * @return void
357                  */
358                 function scheduleNextTick() {
359                         var delta = time() - settings.lastTick,
360                                 interval = settings.mainInterval;
361
362                         if ( settings.suspend ) {
363                                 return;
364                         }
365
366                         if ( ! settings.hasFocus ) {
367                                 interval = 120000; // 120 sec. Post locks expire after 150 sec.
368                         } else if ( settings.countdown > 0 && settings.tempInterval ) {
369                                 interval = settings.tempInterval;
370                                 settings.countdown--;
371
372                                 if ( settings.countdown < 1 ) {
373                                         settings.tempInterval = 0;
374                                 }
375                         }
376
377                         window.clearTimeout( settings.beatTimer );
378
379                         if ( delta < interval ) {
380                                 settings.beatTimer = window.setTimeout(
381                                         function() {
382                                                         connect();
383                                         },
384                                         interval - delta
385                                 );
386                         } else {
387                                 connect();
388                         }
389                 }
390
391                 /**
392                  * Set the internal state when the browser window looses focus
393                  *
394                  * @access private
395                  *
396                  * @return void
397                  */
398                 function blurred() {
399                         clearFocusTimers();
400                         settings.hasFocus = false;
401                 }
402
403                 /**
404                  * Set the internal state when the browser window is focused
405                  *
406                  * @access private
407                  *
408                  * @return void
409                  */
410                 function focused() {
411                         clearFocusTimers();
412                         settings.userActivity = time();
413
414                         // Resume if suspended
415                         settings.suspend = false;
416
417                         if ( ! settings.hasFocus ) {
418                                 settings.hasFocus = true;
419                                 scheduleNextTick();
420                         }
421                 }
422
423                 /**
424                  * Add focus/blur events to all local iframes
425                  *
426                  * Used to detect when focus is moved from the main window to an iframe
427                  *
428                  * @access private
429                  *
430                  * @return void
431                  */
432                 function setFrameFocusEvents() {
433                         $('iframe').each( function( i, frame ) {
434                                 if ( ! isLocalFrame( frame ) ) {
435                                         return;
436                                 }
437
438                                 if ( $.data( frame, 'wp-heartbeat-focus' ) ) {
439                                         return;
440                                 }
441
442                                 $.data( frame, 'wp-heartbeat-focus', 1 );
443
444                                 $( frame.contentWindow ).on( 'focus.wp-heartbeat-focus', function() {
445                                         focused();
446                                 }).on('blur.wp-heartbeat-focus', function() {
447                                         setFrameFocusEvents();
448                                         // We don't know why 'blur' was fired. Either the user clicked in the main window or outside the browser.
449                                         // Running blurred() after some timeout lets us cancel it if the user clicked in the main window.
450                                         settings.frameBlurTimer = window.setTimeout( function(){ blurred(); }, 500 );
451                                 });
452                         });
453                 }
454
455                 /**
456                  * Remove the focus/blur events to all local iframes
457                  *
458                  * @access private
459                  *
460                  * @return void
461                  */
462                 function removeFrameFocusEvents() {
463                         $('iframe').each( function( i, frame ) {
464                                 if ( ! isLocalFrame( frame ) ) {
465                                         return;
466                                 }
467
468                                 $.removeData( frame, 'wp-heartbeat-focus' );
469                                 $( frame.contentWindow ).off( '.wp-heartbeat-focus' );
470                         });
471                 }
472
473                 /**
474                  * Clear the reset timers for focus/blur events on the window and iframes
475                  *
476                  * @access private
477                  *
478                  * @return void
479                  */
480                 function clearFocusTimers() {
481                         window.clearTimeout( settings.winBlurTimer );
482                         window.clearTimeout( settings.frameBlurTimer );
483                 }
484
485                 /**
486                  * Runs when the user becomes active after a period of inactivity
487                  *
488                  * @access private
489                  *
490                  * @return void
491                  */
492                 function userIsActive() {
493                         settings.userActivityEvents = false;
494                         $document.off( '.wp-heartbeat-active' );
495
496                         $('iframe').each( function( i, frame ) {
497                                 if ( ! isLocalFrame( frame ) ) {
498                                         return;
499                                 }
500
501                                 $( frame.contentWindow ).off( '.wp-heartbeat-active' );
502                         });
503
504                         focused();
505                 }
506
507                 /**
508                  * Check for user activity
509                  *
510                  * Runs every 30 sec.
511                  * Sets 'hasFocus = true' if user is active and the window is in the background.
512                  * Set 'hasFocus = false' if the user has been inactive (no mouse or keyboard activity)
513                  * for 5 min. even when the window has focus.
514                  *
515                  * @access private
516                  *
517                  * @return void
518                  */
519                 function checkUserActivity() {
520                         var lastActive = settings.userActivity ? time() - settings.userActivity : 0;
521
522                         if ( lastActive > 300000 && settings.hasFocus ) {
523                                 // Throttle down when no mouse or keyboard activity for 5 min
524                                 blurred();
525                         }
526
527                         if ( settings.suspendEnabled && lastActive > 1200000 ) {
528                                 // Suspend after 20 min. of inactivity
529                                 settings.suspend = true;
530                         }
531
532                         if ( ! settings.userActivityEvents ) {
533                                 $document.on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } );
534
535                                 $('iframe').each( function( i, frame ) {
536                                         if ( ! isLocalFrame( frame ) ) {
537                                                 return;
538                                         }
539
540                                         $( frame.contentWindow ).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } );
541                                 });
542
543                                 settings.userActivityEvents = true;
544                         }
545                 }
546
547                 // Public methods
548
549                 /**
550                  * Whether the window (or any local iframe in it) has focus, or the user is active
551                  *
552                  * @return bool
553                  */
554                 function hasFocus() {
555                         return settings.hasFocus;
556                 }
557
558                 /**
559                  * Whether there is a connection error
560                  *
561                  * @return bool
562                  */
563                 function hasConnectionError() {
564                         return settings.connectionError;
565                 }
566
567                 /**
568                  * Connect asap regardless of 'hasFocus'
569                  *
570                  * Will not open two concurrent connections. If a connection is in progress,
571                  * will connect again immediately after the current connection completes.
572                  *
573                  * @return void
574                  */
575                 function connectNow() {
576                         settings.lastTick = 0;
577                         scheduleNextTick();
578                 }
579
580                 /**
581                  * Disable suspending
582                  *
583                  * Should be used only when Heartbeat is performing critical tasks like autosave, post-locking, etc.
584                  * Using this on many screens may overload the user's hosting account if several
585                  * browser windows/tabs are left open for a long time.
586                  *
587                  * @return void
588                  */
589                 function disableSuspend() {
590                         settings.suspendEnabled = false;
591                 }
592
593                 /**
594                  * Get/Set the interval
595                  *
596                  * When setting to 'fast' or 5, by default interval is 5 sec. for the next 30 ticks (for 2 min and 30 sec).
597                  * In this case the number of 'ticks' can be passed as second argument.
598                  * If the window doesn't have focus, the interval slows down to 2 min.
599                  *
600                  * @param mixed speed Interval: 'fast' or 5, 15, 30, 60
601                  * @param string ticks Used with speed = 'fast' or 5, how many ticks before the interval reverts back
602                  * @return int Current interval in seconds
603                  */
604                 function interval( speed, ticks ) {
605                         var newInterval,
606                                 oldInterval = settings.tempInterval ? settings.tempInterval : settings.mainInterval;
607
608                         if ( speed ) {
609                                 switch ( speed ) {
610                                         case 'fast':
611                                         case 5:
612                                                 newInterval = 5000;
613                                                 break;
614                                         case 15:
615                                                 newInterval = 15000;
616                                                 break;
617                                         case 30:
618                                                 newInterval = 30000;
619                                                 break;
620                                         case 60:
621                                                 newInterval = 60000;
622                                                 break;
623                                         case 'long-polling':
624                                                 // Allow long polling, (experimental)
625                                                 settings.mainInterval = 0;
626                                                 return 0;
627                                         default:
628                                                 newInterval = settings.originalInterval;
629                                 }
630
631                                 if ( 5000 === newInterval ) {
632                                         ticks = parseInt( ticks, 10 ) || 30;
633                                         ticks = ticks < 1 || ticks > 30 ? 30 : ticks;
634
635                                         settings.countdown = ticks;
636                                         settings.tempInterval = newInterval;
637                                 } else {
638                                         settings.countdown = 0;
639                                         settings.tempInterval = 0;
640                                         settings.mainInterval = newInterval;
641                                 }
642
643                                 // Change the next connection time if new interval has been set.
644                                 // Will connect immediately if the time since the last connection
645                                 // is greater than the new interval.
646                                 if ( newInterval !== oldInterval ) {
647                                         scheduleNextTick();
648                                 }
649                         }
650
651                         return settings.tempInterval ? settings.tempInterval / 1000 : settings.mainInterval / 1000;
652                 }
653
654                 /**
655                  * Enqueue data to send with the next XHR
656                  *
657                  * As the data is send asynchronously, this function doesn't return the XHR response.
658                  * To see the response, use the custom jQuery event 'heartbeat-tick' on the document, example:
659                  *              $(document).on( 'heartbeat-tick.myname', function( event, data, textStatus, jqXHR ) {
660                  *                      // code
661                  *              });
662                  * If the same 'handle' is used more than once, the data is not overwritten when the third argument is 'true'.
663                  * Use wp.heartbeat.isQueued('handle') to see if any data is already queued for that handle.
664                  *
665                  * $param string handle Unique handle for the data. The handle is used in PHP to receive the data.
666                  * $param mixed data The data to send.
667                  * $param bool noOverwrite Whether to overwrite existing data in the queue.
668                  * $return bool Whether the data was queued or not.
669                  */
670                 function enqueue( handle, data, noOverwrite ) {
671                         if ( handle ) {
672                                 if ( noOverwrite && this.isQueued( handle ) ) {
673                                         return false;
674                                 }
675
676                                 settings.queue[handle] = data;
677                                 return true;
678                         }
679                         return false;
680                 }
681
682                 /**
683                  * Check if data with a particular handle is queued
684                  *
685                  * $param string handle The handle for the data
686                  * $return bool Whether some data is queued with this handle
687                  */
688                 function isQueued( handle ) {
689                         if ( handle ) {
690                                 return settings.queue.hasOwnProperty( handle );
691                         }
692                 }
693
694                 /**
695                  * Remove data with a particular handle from the queue
696                  *
697                  * $param string handle The handle for the data
698                  * $return void
699                  */
700                 function dequeue( handle ) {
701                         if ( handle ) {
702                                 delete settings.queue[handle];
703                         }
704                 }
705
706                 /**
707                  * Get data that was enqueued with a particular handle
708                  *
709                  * $param string handle The handle for the data
710                  * $return mixed The data or undefined
711                  */
712                 function getQueuedItem( handle ) {
713                         if ( handle ) {
714                                 return this.isQueued( handle ) ? settings.queue[handle] : undefined;
715                         }
716                 }
717
718                 initialize();
719
720                 // Expose public methods
721                 return {
722                         hasFocus: hasFocus,
723                         connectNow: connectNow,
724                         disableSuspend: disableSuspend,
725                         interval: interval,
726                         hasConnectionError: hasConnectionError,
727                         enqueue: enqueue,
728                         dequeue: dequeue,
729                         isQueued: isQueued,
730                         getQueuedItem: getQueuedItem
731                 };
732         };
733
734         // Ensure the global `wp` object exists.
735         window.wp = window.wp || {};
736         window.wp.heartbeat = new Heartbeat();
737
738 }( jQuery, window ));