[ Index ]

WordPress Cross Reference

title

Body

[close]

/wp-includes/js/ -> heartbeat.js (source)

   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 ));


Generated: Tue Mar 25 01:41:18 2014 WordPress honlapkészítés: online1.hu