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