Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 352 lines (272 sloc) 11.171 kb
8ff4206 @mjangda v1
mjangda authored
1 <?php
2 /**
3 * Plugin Name: Liveblog
4 * Description: Blogging: at the speed of live.
5 * Version: 0.1
6 * Author: WordPress.com VIP, Automattic
7 */
8
9 /*
10 TODO (0.1):
b12c4b9 @mjangda Adding inline comments and TODOs
mjangda authored
11 -- Fix Batcache issues
8ff4206 @mjangda v1
mjangda authored
12
13 TODO (future):
b12c4b9 @mjangda Adding inline comments and TODOs
mjangda authored
14 -- PHP and JS Actions/Filters/Triggers
15 -- Change "Read More" to "View Liveblog"
8ff4206 @mjangda v1
mjangda authored
16 -- Manual refresh button
17 -- Allow marking of liveblog as ended
18 -- Allow comment modifications; need to store modified date as comment_meta
19 -- Drag-and-drop image uploading support
20
21 */
22
675af15 @nb Move WPCOM_Liveblog_Entry to a separate file
nb authored
23 require dirname( __FILE__ ) . '/class-wpcom-liveblog-entry.php';
96d01d2 @nb Move WPCOM_Liveblog_Entries to its own file
nb authored
24 require dirname( __FILE__ ) . '/class-wpcom-liveblog-entries.php';
25
8ff4206 @mjangda v1
mjangda authored
26 if ( ! class_exists( 'WPCOM_Liveblog' ) ) :
27
28 class WPCOM_Liveblog {
29
30 const version = 0.1;
31 const key = 'liveblog';
32 const url_endpoint = 'liveblog';
33 const edit_cap = 'publish_posts';
34 const nonce_key = 'liveblog_nonce';
35
8ce95e9 @nb Use 3 second refresh
nb authored
36 const refresh_interval = 3; // how often should we refresh
b12c4b9 @mjangda Adding inline comments and TODOs
mjangda authored
37 const max_retries = 100; // max number of failed tries before polling is disabled
38 const delay_threshold = 10; // how many failed tries after which we should increase the refresh interval
39 const delay_multiplier = 1.5; // by how much should we inscrease the refresh interval
8ff4206 @mjangda v1
mjangda authored
40
4725359 @nb Extract entry template in a separate file
nb authored
41
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
42 static $post_id = null;
43 static $entries = null;
6c66757 @nb Do not let the client cache future requests
nb authored
44 static $do_not_cache_response = false;
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
45
8ff4206 @mjangda v1
mjangda authored
46 function load() {
47 add_action( 'init', array( __CLASS__, 'init' ) );
48 add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
49
50 add_action( 'wp_ajax_liveblog_insert_entry', array( __CLASS__, 'ajax_insert_entry' ) );
51
52 add_filter( 'template_redirect', array( __CLASS__, 'handle_request' ) );
53
54 add_filter( 'comment_class', array( __CLASS__, 'add_comment_class' ) );
55
56 add_action( 'add_meta_boxes', array( __CLASS__, 'add_meta_box' ) );
57 add_action( 'save_post', array( __CLASS__, 'save_meta_box' ) );
58 }
59
60 function init() {
b12c4b9 @mjangda Adding inline comments and TODOs
mjangda authored
61 // TODO filters for time and interval overrides
62
8ff4206 @mjangda v1
mjangda authored
63 add_rewrite_endpoint( self::url_endpoint, EP_PERMALINK ); // /2012/01/01/post-name/liveblog/123456/ where 123456 is a timestamp
64
65 add_post_type_support( 'post', self::key );
66 }
67
68 function handle_request( $query ) {
69 if ( ! self::is_viewing_liveblog_post() )
70 return;
71
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
72 self::$post_id = get_the_ID();
73 self::$entries = new WPCOM_Liveblog_Entries( self::$post_id, self::key );
74
75 if ( self::is_initial_page_request() ) {
8ff4206 @mjangda v1
mjangda authored
76 add_filter( 'the_content', array( __CLASS__, 'add_liveblog_to_content' ) );
77 return;
78 }
79
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
80 if ( self::is_entries_ajax_request() ) {
7c3d06f @nb Rename the entries AJAX handler to match the other
nb authored
81 self::ajax_entries_between();
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
82 return;
83 }
84
85 wp_safe_redirect( get_permalink() );
86 exit;
8ff4206 @mjangda v1
mjangda authored
87 }
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
88
7c3d06f @nb Rename the entries AJAX handler to match the other
nb authored
89 function ajax_entries_between() {
558008c @nb Use the same current timestamp at all places
nb authored
90 $current_timestamp = time();
91
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
92 list( $start_timestamp, $end_timestamp ) = self::get_timestamps_from_query();
93 if ( !$end_timestamp ) {
94 wp_safe_redirect( get_permalink() );
95 }
96
6c66757 @nb Do not let the client cache future requests
nb authored
97 if ( $end_timestamp > $current_timestamp ) {
98 self::$do_not_cache_response = true;
99 }
100
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
101 $entries = self::$entries->get_between_timestamps( $start_timestamp, $end_timestamp );
102 if ( !$entries ) {
e977561 @nb Get rid of extra json_return() arguments
nb authored
103 self::json_return( array( 'entries' => array(), 'current_timestamp' => $current_timestamp, 'latest_timestamp' => null ) );
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
104 }
d79a82d @nb Gather common entry functionality in a new class
nb authored
105
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
106 $latest_timestamp = 0;
d79a82d @nb Gather common entry functionality in a new class
nb authored
107
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
108 $entries_for_json = array();
109 foreach( $entries as $entry ) {
d79a82d @nb Gather common entry functionality in a new class
nb authored
110 $latest_timestamp = max( $latest_timestamp, $entry->get_timestamp() );
111 $entries_for_json[] = $entry->for_json();
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
112 }
113
114 $result_for_json = array(
115 'entries' => $entries_for_json,
558008c @nb Use the same current timestamp at all places
nb authored
116 'current_timestamp' => $current_timestamp,
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
117 'latest_timestamp' => $latest_timestamp,
118 );
119
e977561 @nb Get rid of extra json_return() arguments
nb authored
120 self::json_return( $result_for_json );
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
121 }
122
123 function is_viewing_liveblog_post() {
124 return is_single() && self::is_liveblog_post();
125 }
126
127 function is_initial_page_request() {
8ff4206 @mjangda v1
mjangda authored
128 global $wp_query;
129 // Not using get_query_var since it returns '' for all requests, which is a valid for /post-name/liveblog/
130 return ! isset( $wp_query->query_vars['liveblog'] );
131 }
1b8e8d5 @nb Streamline logic for getting recent entries
nb authored
132
133
134 function is_entries_ajax_request() {
135 return (bool)get_query_var( self::url_endpoint );
136 }
137
138 function get_timestamps_from_query() {
139 $timestamps = explode( '/', get_query_var( self::url_endpoint) );
140 if ( 2 != count( $timestamps ) ) {
141 return array( false, false );
142 }
143 $timestamps = array_map( 'intval', $timestamps );
144 return $timestamps;
145 }
146
8ff4206 @mjangda v1
mjangda authored
147 function ajax_insert_entry() {
b58c3a0 @nb Remove leading underscores from function names
nb authored
148 self::ajax_current_user_can_edit_liveblog();
149 self::ajax_check_nonce();
8ff4206 @mjangda v1
mjangda authored
150
151 $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
152
153 if ( ! $post_id )
d81e042 @nb Extract sending errors in its own method
nb authored
154 self::send_error( __( "Sorry, that's an invalid post_id", 'liveblog' ) );
8ff4206 @mjangda v1
mjangda authored
155
156 $user = wp_get_current_user();
157
158 $entry_content = wp_filter_post_kses( $_POST['entry_content'] ); // these should have the same kses rules as posts
159
160 $entry = array(
161 'comment_post_ID' => $post_id,
162 'comment_content' => $entry_content,
2ba7f86 @nb Remove trailing whitespace
nb authored
163
8ff4206 @mjangda v1
mjangda authored
164 'comment_approved' => self::key,
165 'comment_type' => self::key,
166
167 'user_id' => $user->ID,
168 // TODO: Should we be adding this or generating dynamically?
169 'comment_author' => $user->display_name,
170 'comment_author_email' => $user->user_email,
171 'comment_author_url' => $user->user_url,
172 // Borrowed from core as wp_insert_comment does not generate them
173 'comment_author_IP' => preg_replace( '/[^0-9a-fA-F:., ]/', '',$_SERVER['REMOTE_ADDR'] ),
174 'comment_agent' => substr( $_SERVER['HTTP_USER_AGENT'], 0, 254 ),
175 );
176
976c894 @nb Send error if comment wasn't created
nb authored
177 $new_comment_id = wp_insert_comment( $entry );
8ff4206 @mjangda v1
mjangda authored
178
976c894 @nb Send error if comment wasn't created
nb authored
179 if ( !$new_comment_id ) {
d81e042 @nb Extract sending errors in its own method
nb authored
180 self::send_error( __( 'Error posting entry!' ) );
976c894 @nb Send error if comment wasn't created
nb authored
181 }
a2f3ce4 @nb Show inserted entry immediately
nb authored
182
183 $entry = WPCOM_Liveblog_Entry::from_comment( get_comment( $new_comment_id ) );
184
185 // do not send latest_timestamp, because if we send it the client won't try to get older entries, but since we now send only the inserted
186 // one we don't know if there weren't any entries in between
e977561 @nb Get rid of extra json_return() arguments
nb authored
187 self::json_return( array( 'entries' => array( $entry->for_json() ), 'current_timestamp' => time(), 'latest_timestamp' => null ) );
8ff4206 @mjangda v1
mjangda authored
188 }
189
7218300 @nb Remove echo functionality for entry_output()
nb authored
190 function entry_output( $entry ) {
8ff4206 @mjangda v1
mjangda authored
191 }
192
193 function add_comment_class( $classes ) {
194 $classes[] = 'liveblog-entry';
195 return $classes;
196 }
197
198 function enqueue_scripts() {
199 if ( ! self::is_viewing_liveblog_post() )
200 return;
201
202 wp_enqueue_script( 'liveblog', plugins_url( 'js/liveblog.js', __FILE__ ), array( 'jquery' ), self::version, true );
203 wp_enqueue_style( 'liveblog', plugins_url( 'css/liveblog.css', __FILE__ ) );
204
b58c3a0 @nb Remove leading underscores from function names
nb authored
205 if ( self::current_user_can_edit_liveblog() )
8ff4206 @mjangda v1
mjangda authored
206 wp_enqueue_script( 'liveblog-publisher', plugins_url( 'js/liveblog-publisher.js', __FILE__ ), array( 'liveblog' ), self::version, true );
207
c0c3085 @nb Add spinner for inserting an entry
nb authored
208 if ( wp_script_is( 'jquery.spin', 'registered' ) ) {
209 wp_enqueue_script( 'jquery.spin' );
210 } else {
211 wp_enqueue_script( 'spin', plugins_url( 'js/spin.js', __FILE__ ), false, '1.2.4' );
212 wp_enqueue_script( 'jquery.spin', plugins_url( 'js/jquery.spin.js', __FILE__ ), array( 'jquery', 'spin' ) );
213 }
214
8ff4206 @mjangda v1
mjangda authored
215 $liveblog_settings = apply_filters( 'liveblog_settings', array(
216 'key' => self::key,
217 'nonce_key' => self::nonce_key,
218 'permalink' => get_permalink(),
219 'post_id' => get_the_ID(),
c7ad98a @nb Don't bother to cache latest entry timestamp
nb authored
220 'latest_entry_timestamp' => self::$entries->get_latest_timestamp(),
8ff4206 @mjangda v1
mjangda authored
221
222 'refresh_interval' => self::refresh_interval,
223 'max_retries' => self::max_retries,
224 'delay_threshold' => self::delay_threshold,
225 'delay_multiplier' => self::delay_multiplier,
226
227 'ajaxurl' => admin_url( 'admin-ajax.php' ),
228 'entriesurl' => self::get_entries_endpoint_url(),
229
230 // i18n
231 'update_nag_singular' => __( '%d new update', 'liveblog' ), // TODO: is there a better way to do _n via js?
232 'update_nag_plural' => __( '%d new updates', 'liveblog' ),
233 ) );
234 wp_localize_script( 'liveblog', 'liveblog_settings', $liveblog_settings );
235 }
236
237 function add_liveblog_to_content( $content ) {
5539d43 @nb Use self::$entries instead of self::get_entries*
nb authored
238 $entries = self::$entries->get( array( 'order' => 'ASC' ) );
8870ab9 @mjangda Reverse the entries array when adding updates from the server so that…
mjangda authored
239 $entries = array_reverse( $entries );
cb778e4 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
240
8ff4206 @mjangda v1
mjangda authored
241 $liveblog_output = '';
5539d43 @nb Use self::$entries instead of self::get_entries*
nb authored
242 $liveblog_output .= '<div id="liveblog-'. self::$post_id .'" class="liveblog-container">';
273c386 @nb Show a spinner when updating entries
nb authored
243 $liveblog_output .= '<div id="liveblog-update-spinner"></div>';
bce6245 @nb Always add to $liveblog_output, don't sometimes assign
nb authored
244 $liveblog_output .= '<div class="liveblog-actions">';
8ff4206 @mjangda v1
mjangda authored
245 $liveblog_output .= self::get_entry_editor_output();
bce6245 @nb Always add to $liveblog_output, don't sometimes assign
nb authored
246 $liveblog_output .= '</div>';
ba2c2ae @mjangda Switch around ids with classes just to be future-safe. Add a new wrap…
mjangda authored
247 $liveblog_output .= '<div class="liveblog-entries">';
cb778e4 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
248 foreach ( (array) $entries as $entry ) {
d79a82d @nb Gather common entry functionality in a new class
nb authored
249 $liveblog_output .= $entry->render();
cb778e4 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
250 }
251
252 $liveblog_output .= '</div>';
ba2c2ae @mjangda Switch around ids with classes just to be future-safe. Add a new wrap…
mjangda authored
253 $liveblog_output .= '</div>';
8ff4206 @mjangda v1
mjangda authored
254
255 return $content . $liveblog_output;
256 }
257
258 function get_entry_editor_output() {
b58c3a0 @nb Remove leading underscores from function names
nb authored
259 if ( ! self::current_user_can_edit_liveblog() )
8ff4206 @mjangda v1
mjangda authored
260 return;
261
262 $editor_output = '';
263 $editor_output .= '<textarea id="liveblog-form-entry" name="liveblog-form-entry"></textarea>';
890df38 @nb Escape translated label in attribute
nb authored
264 $editor_output .= '<input type="button" id="liveblog-form-entry-submit" class="button" value="'. esc_attr__( 'Post Update' ) . '" />';
c0c3085 @nb Add spinner for inserting an entry
nb authored
265 $editor_output .= '<span id="liveblog-submit-spinner"></span>';
8ff4206 @mjangda v1
mjangda authored
266 $editor_output .= wp_nonce_field( self::nonce_key, self::nonce_key, false, false );
267
268 return $editor_output;
269 }
270
271
272 function is_liveblog_post( $post_id = null ) {
273 if ( empty( $post_id ) ) {
274 global $post;
275 $post_id = $post->ID;
276 }
277 return get_post_meta( $post_id, self::key, true );
278 }
279
280 function add_meta_box( $post_type ) {
281 if ( post_type_supports( $post_type, self::key ) )
282 add_meta_box( self::key, __( 'Liveblog', 'liveblog' ), array( __CLASS__, 'display_meta_box' ) );
283 }
284 function display_meta_box( $post ) {
285 ?>
286 <label>
287 <input type="checkbox" name="is-liveblog" id="is-liveblog" value="1" <?php checked( self::is_liveblog_post( $post->ID ) ); ?> />
288 <?php esc_html_e( 'This is a liveblog', 'liveblog' ); ?>
289 </label>
290 <?php
291 wp_nonce_field( 'liveblog_nonce', 'liveblog_nonce', false );
292 }
293 function save_meta_box( $post_id ) {
294 if ( ! isset( $_POST['liveblog_nonce'] ) || ! wp_verify_nonce( $_POST['liveblog_nonce'], 'liveblog_nonce' ) )
295 return;
296
297 if ( isset( $_POST['is-liveblog'] ) )
298 update_post_meta( $post_id, self::key, 1 );
299 else
300 delete_post_meta( $post_id, self::key );
301 }
2ba7f86 @nb Remove trailing whitespace
nb authored
302
b897ffd @nb Remove arguments of get_entries_endpoint_url()
nb authored
303 function get_entries_endpoint_url() {
304 return trailingslashit( get_permalink( self::$post_id ) . self::url_endpoint );
8ff4206 @mjangda v1
mjangda authored
305 }
306
b58c3a0 @nb Remove leading underscores from function names
nb authored
307 function ajax_current_user_can_edit_liveblog() {
308 if ( ! self::current_user_can_edit_liveblog() ) {
d4aa5ad @nb Use double quotes instead of escaping the apostrophe
nb authored
309 self::send_error( __( "Cheatin', uh?", 'liveblog' ) );
8ff4206 @mjangda v1
mjangda authored
310 }
311 }
b58c3a0 @nb Remove leading underscores from function names
nb authored
312 function current_user_can_edit_liveblog() {
8ff4206 @mjangda v1
mjangda authored
313 return current_user_can( apply_filters( 'liveblog_edit_cap', self::edit_cap ) );
314 }
315
b58c3a0 @nb Remove leading underscores from function names
nb authored
316 function ajax_check_nonce( $action = 'liveblog_nonce' ) {
8ff4206 @mjangda v1
mjangda authored
317 if ( ! isset( $_REQUEST[ self::nonce_key ] ) || ! wp_verify_nonce( $_REQUEST[ self::nonce_key ], $action ) ) {
d81e042 @nb Extract sending errors in its own method
nb authored
318 self::send_error( __( 'Sorry, we could not authenticate you.', 'liveblog' ) );
8ff4206 @mjangda v1
mjangda authored
319 }
320 }
321
e977561 @nb Get rid of extra json_return() arguments
nb authored
322 function json_return( $data ) {
20c5b02 @nb Get rid of the intermediate layer completely
nb authored
323 $json_data = json_encode( $data );
8ff4206 @mjangda v1
mjangda authored
324
f2908df @nb Get rid if return functionality in json_return()
nb authored
325 header( 'Content-Type: application/json' );
6c66757 @nb Do not let the client cache future requests
nb authored
326 if ( self::$do_not_cache_response ) {
327 nocache_headers();
328 }
20c5b02 @nb Get rid of the intermediate layer completely
nb authored
329
330 echo $json_data;
f2908df @nb Get rid if return functionality in json_return()
nb authored
331 exit;
8ff4206 @mjangda v1
mjangda authored
332 }
333
d81e042 @nb Extract sending errors in its own method
nb authored
334 function send_error( $message ) {
335 self::status_header_with_message( 500, $message );
336 exit;
337 }
338
2532397 @nb Send HTTP errors instead of the success flag
nb authored
339 function status_header_with_message( $status, $message ) {
340 global $wp_header_to_desc;
341 $status = absint( $status );
342 $official_message = isset( $wp_header_to_desc[$status] )? $wp_header_to_desc[$status] : '';
343 $wp_header_to_desc[$status] = $message;
344 status_header( $status );
345 $wp_header_to_desc[$status] = $official_message;
346 }
347
8ff4206 @mjangda v1
mjangda authored
348 }
349
350 WPCOM_Liveblog::load();
351 endif;
Something went wrong with that request. Please try again.