From 98bf511b5680d6808babe57fa3614acffa01f67d Mon Sep 17 00:00:00 2001 From: Andrew Ozz Date: Tue, 29 Jan 2013 06:15:25 +0000 Subject: [PATCH] Heartbeat API: first run, see #23216 git-svn-id: http://core.svn.wordpress.org/trunk@23355 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/admin-ajax.php | 2 +- wp-admin/includes/ajax-actions.php | 29 +++++ wp-includes/default-filters.php | 3 + wp-includes/general-template.php | 15 +++ wp-includes/js/heartbeat.js | 178 +++++++++++++++++++++++++++++ wp-includes/script-loader.php | 7 +- 6 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 wp-includes/js/heartbeat.js diff --git a/wp-admin/admin-ajax.php b/wp-admin/admin-ajax.php index 716e384da4..9f5f025b4c 100644 --- a/wp-admin/admin-ajax.php +++ b/wp-admin/admin-ajax.php @@ -56,7 +56,7 @@ $core_actions_post = array( 'save-widget', 'set-post-thumbnail', 'date_format', 'time_format', 'wp-fullscreen-save-post', 'wp-remove-post-lock', 'dismiss-wp-pointer', 'upload-attachment', 'get-attachment', 'query-attachments', 'save-attachment', 'save-attachment-compat', 'send-link-to-editor', - 'send-attachment-to-editor', 'save-attachment-order', + 'send-attachment-to-editor', 'save-attachment-order', 'heartbeat', ); // Register core Ajax calls. diff --git a/wp-admin/includes/ajax-actions.php b/wp-admin/includes/ajax-actions.php index 7022c470b1..7704ba91f3 100644 --- a/wp-admin/includes/ajax-actions.php +++ b/wp-admin/includes/ajax-actions.php @@ -2071,3 +2071,32 @@ function wp_ajax_send_link_to_editor() { wp_send_json_success( $html ); } + +function wp_ajax_heartbeat() { + check_ajax_referer( 'heartbeat-nonce', '_nonce' ); + $response = array( 'pagenow' => '' ); + + if ( ! empty($_POST['pagenow']) ) + $response['pagenow'] = sanitize_key($_POST['pagenow']); + + if ( ! empty($_POST['data']) ) { + $data = (array) $_POST['data']; + // todo: how much to sanitize and preset and what to leave to be accessed from $data or $_POST..? + $user = wp_get_current_user(); + $data['user_id'] = $user->exists() ? $user->ID : 0; + + // todo: separate filters: 'heartbeat_[action]' so we call different callbacks only when there is data for them, + // or all callbacks listen to one filter and run when there is something for them in $data? + $response = apply_filters( 'heartbeat_received', $response, $data ); + } + + $response = apply_filters( 'heartbeat_send', $response ); + + // Allow the transport to be replaced with long-polling easily + do_action( 'heartbeat_tick', $response ); + + // always send the current time acording to the server + $response['time'] = time(); + + wp_send_json($response); +} diff --git a/wp-includes/default-filters.php b/wp-includes/default-filters.php index cc6e07fa97..7d47e45316 100644 --- a/wp-includes/default-filters.php +++ b/wp-includes/default-filters.php @@ -290,4 +290,7 @@ add_filter( 'default_option_link_manager_enabled', '__return_true' ); // This option no longer exists; tell plugins we always support auto-embedding. add_filter( 'default_option_embed_autourls', '__return_true' ); +// Default settings for heartbeat +add_filter( 'heartbeat_settings', 'wp_heartbeat_settings' ); + unset($filter, $action); diff --git a/wp-includes/general-template.php b/wp-includes/general-template.php index af3d572e59..da39358582 100644 --- a/wp-includes/general-template.php +++ b/wp-includes/general-template.php @@ -2284,3 +2284,18 @@ function __checked_selected_helper( $helper, $current, $echo, $type ) { return $result; } + +/** + * Default settings for heartbeat + * + * Outputs the nonce used in the heartbeat XHR + * + * @since 3.6.0 + * + * @param array $settings + * @return array $settings + */ +function wp_heartbeat_settings( $settings ) { + $setting['nonce'] = wp_create_nonce( 'heartbeat-nonce' ); + return $setting; +} diff --git a/wp-includes/js/heartbeat.js b/wp-includes/js/heartbeat.js new file mode 100644 index 0000000000..c31107f9cc --- /dev/null +++ b/wp-includes/js/heartbeat.js @@ -0,0 +1,178 @@ +/** + * Heartbeat API + */ + + // Ensure the global `wp` object exists. +window.wp = window.wp || {}; + +(function($){ + var Heartbeat = function() { + var self = this, + running, + timeout, + nonce, + screen = typeof pagenow != 'undefined' ? pagenow : '', + settings, + tick = 0, + queue = {}, + interval, + lastconnect = 0; + + this.url = typeof ajaxurl != 'undefined' ? ajaxurl : 'wp-admin/admin-ajax.php'; + this.autostart = true; + + if ( typeof( window.heartbeatSettings != 'undefined' ) ) { + settings = $.extend( {}, window.heartbeatSettings ); + delete window.heartbeatSettings; + + // Add private vars + nonce = settings.nonce || ''; + delete settings.nonce; + + interval = settings.interval || 15000; // default interval + delete settings.interval; + + // todo: needed? + // 'pagenow' can be added from settings if not already defined + screen = screen || settings.pagenow; + delete settings.pagenow; + + // Add public vars + $.extend( this, settings ); + } + + function time(s) { + if ( s ) + return parseInt( (new Date()).getTime() / 1000 ); + + return (new Date()).getTime(); + } + + // Set error state and fire an event if it persists for over 3 min + function errorstate() { + var since; + + if ( lastconnect ) { + since = time() - lastconnect; + + if ( since > 180000 ) { + self.connectionLost = true; + $(document).trigger( 'heartbeat-connection-lost', parseInt(since / 1000) ); + } else if ( self.connectionLost ) { + self.connectionLost = false; + $(document).trigger( 'heartbeat-connection-restored' ); + } + } + } + + function connect() { + var data = {}; + tick = time(); + + data.data = $.extend( {}, queue ); + queue = {}; + + data.interval = interval / 1000; + data._nonce = nonce; + data.action = 'heartbeat'; + data.pagenow = screen; + + self.xhr = $.post( self.url, data, function(r){ + lastconnect = time(); + // Clear error state + if ( self.connectionLost ) + errorstate(); + + self.tick(r); + }, 'json' ).always( function(){ + next(); + }).fail( function(r){ + errorstate(); + self.error(r); + }); + }; + + function next() { + var delta = time() - tick; + + if ( !running ) + return; + + if ( delta < interval ) { + timeout = window.setTimeout( + function(){ + if ( running ) + connect(); + }, + interval - delta + ); + } else { + window.clearTimeout(timeout); // this has already expired? + connect(); + } + }; + + this.interval = function(seconds) { + if ( seconds ) { + // Limit + if ( 5 > seconds || seconds > 60 ) + return false; + + interval = seconds * 1000; + } else if ( seconds === 0 ) { + // Allow long polling to be turned on + interval = 0; + } + return interval / 1000; + }; + + this.start = function() { + // start only once + if ( running ) + return false; + + running = true; + connect(); + + return true; + }; + + this.stop = function() { + if ( !running ) + return false; + + if ( self.xhr ) + self.xhr.abort(); + + running = false; + return true; + } + + this.send = function(action, data) { + if ( action ) + queue[action] = data; + } + + if ( this.autostart ) { + $(document).ready( function(){ + // Start one tick (15 sec) after DOM ready + running = true; + tick = time(); + next(); + }); + } + + } + + $.extend( Heartbeat.prototype, { + tick: function(r) { + $(document).trigger( 'heartbeat-tick', r ); + }, + error: function(r) { + $(document).trigger( 'heartbeat-error', r ); + } + }); + + wp.heartbeat = new Heartbeat(); + +}(jQuery)); diff --git a/wp-includes/script-loader.php b/wp-includes/script-loader.php index c8aa41065f..9612e72850 100644 --- a/wp-includes/script-loader.php +++ b/wp-includes/script-loader.php @@ -107,6 +107,11 @@ function wp_default_scripts( &$scripts ) { ) ); $scripts->add( 'autosave', "/wp-includes/js/autosave$suffix.js", array('schedule', 'wp-ajax-response'), false, 1 ); + + $scripts->add( 'heartbeat', "/wp-includes/js/heartbeat$suffix.js", array('jquery'), false, 1 ); + did_action( 'init' ) && $scripts->localize( 'heartbeat', 'heartbeatSettings', + apply_filters( 'heartbeat_settings', array() ) + ); $scripts->add( 'wp-lists', "/wp-includes/js/wp-lists$suffix.js", array( 'wp-ajax-response', 'jquery-color' ), false, 1 ); @@ -371,7 +376,7 @@ function wp_default_scripts( &$scripts ) { $scripts->add( 'postbox', "/wp-admin/js/postbox$suffix.js", array('jquery-ui-sortable'), false, 1 ); - $scripts->add( 'post', "/wp-admin/js/post$suffix.js", array('suggest', 'wp-lists', 'postbox'), false, 1 ); + $scripts->add( 'post', "/wp-admin/js/post$suffix.js", array('suggest', 'wp-lists', 'postbox', 'heartbeat'), false, 1 ); did_action( 'init' ) && $scripts->localize( 'post', 'postL10n', array( 'ok' => __('OK'), 'cancel' => __('Cancel'),