Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 440 lines (350 sloc) 14.389 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 -- Loading icon
12 -- Prime caches on comment update (comment count)
13 -- Fix Batcache issues
14 -- Show updates server-side on initial load? (problem is that batcache would show an old set; although, timestamp would also be old so we'd still fetch the necessary entries)
8ff4206 @mjangda v1
mjangda authored
15
16 TODO (future):
b12c4b9 @mjangda Adding inline comments and TODOs
mjangda authored
17 -- PHP and JS Actions/Filters/Triggers
18 -- Change "Read More" to "View Liveblog"
8ff4206 @mjangda v1
mjangda authored
19 -- Manual refresh button
20 -- Allow marking of liveblog as ended
21 -- Allow comment modifications; need to store modified date as comment_meta
22 -- Drag-and-drop image uploading support
23
24 */
25
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
b12c4b9 @mjangda Adding inline comments and TODOs
mjangda authored
36 const refresh_interval = 15; // how often should we refresh
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
41 function load() {
42 add_action( 'init', array( __CLASS__, 'init' ) );
43 add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
44
45 add_action( 'wp_ajax_liveblog_insert_entry', array( __CLASS__, 'ajax_insert_entry' ) );
46
47 add_filter( 'template_redirect', array( __CLASS__, 'handle_request' ) );
48
49 add_filter( 'comment_class', array( __CLASS__, 'add_comment_class' ) );
50
51 add_action( 'add_meta_boxes', array( __CLASS__, 'add_meta_box' ) );
52 add_action( 'save_post', array( __CLASS__, 'save_meta_box' ) );
53 }
54
55 function init() {
b12c4b9 @mjangda Adding inline comments and TODOs
mjangda authored
56 // TODO filters for time and interval overrides
57
8ff4206 @mjangda v1
mjangda authored
58 add_rewrite_endpoint( self::url_endpoint, EP_PERMALINK ); // /2012/01/01/post-name/liveblog/123456/ where 123456 is a timestamp
59
60 add_post_type_support( 'post', self::key );
61 }
62
63 function handle_request( $query ) {
64 if ( ! self::is_viewing_liveblog_post() )
65 return;
66
67 if ( self::is_liveblog_request() ) {
68 add_filter( 'the_content', array( __CLASS__, 'add_liveblog_to_content' ) );
69 return;
70 }
71
72 $timestamp = get_query_var( 'liveblog' );
73 $entries = self::ajax_get_recent_entries( $timestamp );
74 }
75 function is_liveblog_request() {
76 global $wp_query;
77 // Not using get_query_var since it returns '' for all requests, which is a valid for /post-name/liveblog/
78 return ! isset( $wp_query->query_vars['liveblog'] );
79 }
80 function ajax_insert_entry() {
81 self::_ajax_current_user_can_edit_liveblog();
82 self::_ajax_check_nonce();
83
84 $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
85
86 if ( ! $post_id )
87 self::json_return( false, __( 'Sorry, that\'s an invalid post_id', 'liveblog' ) );
88
89 $user = wp_get_current_user();
90
91 $entry_content = wp_filter_post_kses( $_POST['entry_content'] ); // these should have the same kses rules as posts
92
93 $entry = array(
94 'comment_post_ID' => $post_id,
95 'comment_content' => $entry_content,
2ba7f86 @nb Remove trailing whitespace
nb authored
96
8ff4206 @mjangda v1
mjangda authored
97 'comment_approved' => self::key,
98 'comment_type' => self::key,
99
100 'user_id' => $user->ID,
101 // TODO: Should we be adding this or generating dynamically?
102 'comment_author' => $user->display_name,
103 'comment_author_email' => $user->user_email,
104 'comment_author_url' => $user->user_url,
105 // Borrowed from core as wp_insert_comment does not generate them
106 'comment_author_IP' => preg_replace( '/[^0-9a-fA-F:., ]/', '',$_SERVER['REMOTE_ADDR'] ),
107 'comment_agent' => substr( $_SERVER['HTTP_USER_AGENT'], 0, 254 ),
108 );
109
110 wp_insert_comment( $entry );
111
112 self::refresh_last_entry_timestamp();
113
114 self::json_return( true, '' );
115 }
116
117 function ajax_get_recent_entries( $timestamp = 0 ) {
118 // When a new update is added, a cache key should be set/updated to the current GMT timestamp.
119 // The AJAX call can then just check that value and immediately abort if it knows that there is no
120 // new data to get yet without having to actually ask WordPress if there are new posts.
121
122 $last_timestamp = self::get_last_entry_timestamp();
123 if ( $last_timestamp && $timestamp >= $last_timestamp ) {
124 self::json_return( true, '', array( 'timestamp' => $last_timestamp ) );
125 }
126
127 $entries_output = array();
128 $entries = self::get_entries_since( get_the_ID(), $timestamp );
129 $filtered_entries = array();
130
131 foreach( $entries as $entry ) {
132 $filtered_entry = new stdClass;
133 $filtered_entry->ID = $entry->comment_ID;
cb778e4 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
134 $filtered_entry->content = self::entry_output( $entry, false );
8ff4206 @mjangda v1
mjangda authored
135 array_push( $filtered_entries, $filtered_entry );
136 }
137
138 if ( ! $last_timestamp )
139 self::refresh_last_entry_timestamp();
140
141 self::json_return( true, '', array( 'entries' => $filtered_entries, 'timestamp' => $last_timestamp ) );
142 }
143
cb778e4 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
144 function entry_output( $entry, $echo = true ) {
8ff4206 @mjangda v1
mjangda authored
145 $entry_id = $entry->comment_ID;
146 $post_id = $entry->comment_post_ID;
147 $output = '';
148
149 // Allow plugins to override the output
150 $output = apply_filters( 'liveblog_pre_entry_output', $output, $entry );
151 if ( $output )
152 return $output;
153
cb778e4 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
154 $args = apply_filters( 'liveblog_entry_output_args', array(
8ff4206 @mjangda v1
mjangda authored
155 'avatar_size' => 30,
156 ) );
157
158 $output .= '<div id="liveblog-entry-'. $entry_id .'" '. comment_class( '', $entry_id, $post_id, false ) . '>';
159 $output .= '<div class="liveblog-entry-text">';
160 $output .= get_comment_text( $entry_id );
161 $output .= '</div>';
2ba7f86 @nb Remove trailing whitespace
nb authored
162
8ff4206 @mjangda v1
mjangda authored
163 $output .= '<header class="liveblog-meta">';
164 $output .= '<span class="liveblog-author-avatar">';
165 $output .= get_avatar( $entry->comment_author_email, $args['avatar_size'] );
166 $output .= '</span>';
2ba7f86 @nb Remove trailing whitespace
nb authored
167
8ff4206 @mjangda v1
mjangda authored
168 $output .= '<span class="liveblog-author-name">'. get_comment_author_link( $entry_id ) .'</span>';
169 $output .= '<span class="liveblog-meta-time">';
170 $output .= '<a href="#liveblog-entry-'. $entry_id .'">';
171 $output .= sprintf( __('%1$s at %2$s'), get_comment_date( '', $entry_id ), get_comment_date( 'g:i a', $entry_id ) );
172 $output .= '</a>';
173 $output .= '</time>';
174 $output .= '</header>';
175 $output .= '</div>';
176
177 $output = apply_filters( 'liveblog_entry_output', $output, $entry );
178 if ( ! $echo )
179 return $output;
180 echo $output;
181 }
182
183 function add_comment_class( $classes ) {
184 $classes[] = 'liveblog-entry';
185 return $classes;
186 }
187
188 function get_entries_since( $post_id, $timestamp = 0 ) {
189 add_filter( 'comments_clauses', array( __CLASS__, 'comments_where_include_liveblog_status' ), false, 2 );
190 $entries = get_comments( array(
191 'post_id' => $post_id,
192 'orderby' => 'comment_date_gmt',
193 'order' => 'ASC',
b61b38c @mjangda Add "type" arg to to get_comments to avoid cache key collisions
mjangda authored
194 'type' => self::key,
8ff4206 @mjangda v1
mjangda authored
195 ) );
196 remove_filter( 'comments_clauses', array( __CLASS__, 'comments_where_include_liveblog_status' ), false );
197
198 $filtered_entries = array();
199
200 foreach( $entries as $entry ) {
201 if ( $timestamp && strtotime( $entry->comment_date_gmt ) <= $timestamp )
202 continue;
203
204 $filtered_entries[ $entry->comment_ID ] = $entry;
205 }
206 $entries = $filtered_entries;
207
208 return $entries;
209 }
210
211 function comments_where_include_liveblog_status( $clauses, $query ) {
212 global $wpdb;
213 // TODO: should we only fetch the posts we want based on the timestamp in the current context?
214 $clauses[ 'where' ] = $wpdb->prepare( 'comment_post_ID = %d AND comment_type = %s AND comment_approved = %s', $query->query_vars['post_id'], self::key, self::key );
215 return $clauses;
216 }
217
218 function enqueue_scripts() {
219 if ( ! self::is_viewing_liveblog_post() )
220 return;
221
222 wp_enqueue_script( 'liveblog', plugins_url( 'js/liveblog.js', __FILE__ ), array( 'jquery' ), self::version, true );
223 wp_enqueue_style( 'liveblog', plugins_url( 'css/liveblog.css', __FILE__ ) );
224
225 if ( self::_current_user_can_edit_liveblog() )
226 wp_enqueue_script( 'liveblog-publisher', plugins_url( 'js/liveblog-publisher.js', __FILE__ ), array( 'liveblog' ), self::version, true );
227
228 $liveblog_settings = apply_filters( 'liveblog_settings', array(
229 'key' => self::key,
230 'nonce_key' => self::nonce_key,
231 'permalink' => get_permalink(),
232 'post_id' => get_the_ID(),
cb778e4 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
233 'last_timestamp' => self::get_last_entry_timestamp(),
8ff4206 @mjangda v1
mjangda authored
234
235 'refresh_interval' => self::refresh_interval,
236 'max_retries' => self::max_retries,
237 'delay_threshold' => self::delay_threshold,
238 'delay_multiplier' => self::delay_multiplier,
239
240 'ajaxurl' => admin_url( 'admin-ajax.php' ),
241 'entriesurl' => self::get_entries_endpoint_url(),
242
243 // i18n
244 'update_nag_singular' => __( '%d new update', 'liveblog' ), // TODO: is there a better way to do _n via js?
245 'update_nag_plural' => __( '%d new updates', 'liveblog' ),
246 ) );
247 wp_localize_script( 'liveblog', 'liveblog_settings', $liveblog_settings );
248 }
249
250 function add_liveblog_to_content( $content ) {
cb778e4 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
251 $post_id = get_the_ID();
252 $entries = self::get_entries_since( $post_id );
8870ab9 @mjangda Reverse the entries array when adding updates from the server so that…
mjangda authored
253 $entries = array_reverse( $entries );
cb778e4 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
254
8ff4206 @mjangda v1
mjangda authored
255 $liveblog_output = '';
bce6245 @nb Always add to $liveblog_output, don't sometimes assign
nb authored
256 $liveblog_output .= '<div id="liveblog-'. $post_id .'" class="liveblog-container">';
257 $liveblog_output .= '<div class="liveblog-actions">';
8ff4206 @mjangda v1
mjangda authored
258 $liveblog_output .= self::get_entry_editor_output();
bce6245 @nb Always add to $liveblog_output, don't sometimes assign
nb authored
259 $liveblog_output .= '</div>';
ba2c2ae @mjangda Switch around ids with classes just to be future-safe. Add a new wrap…
mjangda authored
260 $liveblog_output .= '<div class="liveblog-entries">';
cb778e4 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
261 foreach ( (array) $entries as $entry ) {
262 $liveblog_output .= self::entry_output( $entry, false );
263 }
264
265 $liveblog_output .= '</div>';
ba2c2ae @mjangda Switch around ids with classes just to be future-safe. Add a new wrap…
mjangda authored
266 $liveblog_output .= '</div>';
8ff4206 @mjangda v1
mjangda authored
267
268 return $content . $liveblog_output;
269 }
270
271 function get_entry_editor_output() {
272 if ( ! self::_current_user_can_edit_liveblog() )
273 return;
274
275 $editor_output = '';
276 $editor_output .= '<textarea id="liveblog-form-entry" name="liveblog-form-entry"></textarea>';
277 //$editor_output .= '<a href="#" id="liveblog-form-entry-submit" class="button">Submit</a>';
278 $editor_output .= '<input type="button" id="liveblog-form-entry-submit" class="button" value="'. __( 'Post Update' ) . '" />';
279 $editor_output .= wp_nonce_field( self::nonce_key, self::nonce_key, false, false );
280
281 return $editor_output;
282 }
283
284 function is_viewing_liveblog_post() {
285 return is_single() && self::is_liveblog_post();
286 }
287
288 function is_liveblog_post( $post_id = null ) {
289 if ( empty( $post_id ) ) {
290 global $post;
291 $post_id = $post->ID;
292 }
293 return get_post_meta( $post_id, self::key, true );
294 }
295
296 function add_meta_box( $post_type ) {
297 if ( post_type_supports( $post_type, self::key ) )
298 add_meta_box( self::key, __( 'Liveblog', 'liveblog' ), array( __CLASS__, 'display_meta_box' ) );
299 }
300 function display_meta_box( $post ) {
301 ?>
302 <label>
303 <input type="checkbox" name="is-liveblog" id="is-liveblog" value="1" <?php checked( self::is_liveblog_post( $post->ID ) ); ?> />
304 <?php esc_html_e( 'This is a liveblog', 'liveblog' ); ?>
305 </label>
306 <?php
307 wp_nonce_field( 'liveblog_nonce', 'liveblog_nonce', false );
308 }
309 function save_meta_box( $post_id ) {
310 if ( ! isset( $_POST['liveblog_nonce'] ) || ! wp_verify_nonce( $_POST['liveblog_nonce'], 'liveblog_nonce' ) )
311 return;
312
313 if ( isset( $_POST['is-liveblog'] ) )
314 update_post_meta( $post_id, self::key, 1 );
315 else
316 delete_post_meta( $post_id, self::key );
317 }
2ba7f86 @nb Remove trailing whitespace
nb authored
318
8ff4206 @mjangda v1
mjangda authored
319
320 function refresh_last_entry_timestamp( $timestamp = null ) {
321 if ( ! $timestamp )
322 $timestamp = current_time( 'timestamp', 1 ); // always work against gmt
323
324 set_transient( 'liveblog-last-entry-timestamp', $timestamp );
325
326 return $timestamp;
327 }
328 function get_last_entry_timestamp() {
329 return get_transient( 'liveblog-last-entry-timestamp' );
330 }
331
332 function get_entries_endpoint_url( $post_id = 0, $timestamp = '' ) {
333 $url = trailingslashit( get_permalink( $post_id ) . self::url_endpoint );
334 if ( $timestamp )
335 $url .= trailingslashit( $timestamp );
336 return $url;
337 }
338
339 function _ajax_current_user_can_edit_liveblog() {
340 if ( ! self::_current_user_can_edit_liveblog() ) {
341 self::json_return( false, __( 'Cheatin\', uh?', 'liveblog' ) );
342 }
343 }
344 function _current_user_can_edit_liveblog() {
345 return current_user_can( apply_filters( 'liveblog_edit_cap', self::edit_cap ) );
346 }
347
348 function _ajax_check_nonce( $action = 'liveblog_nonce' ) {
349 if ( ! isset( $_REQUEST[ self::nonce_key ] ) || ! wp_verify_nonce( $_REQUEST[ self::nonce_key ], $action ) ) {
350 self::json_return( false, __( 'Sorry, we could not authenticate you.', 'liveblog' ) );
351 }
352 }
353
f2908df @nb Get rid if return functionality in json_return()
nb authored
354 function json_return( $success, $message, $data = array() ) {
8ff4206 @mjangda v1
mjangda authored
355 $return = json_encode( array(
356 'status' => intval( $success ),
357 'message' => $message,
358 'data' => $data,
359 ) );
360
f2908df @nb Get rid if return functionality in json_return()
nb authored
361 header( 'Content-Type: application/json' );
362 echo $return;
363 exit;
8ff4206 @mjangda v1
mjangda authored
364 }
365
366 }
367
7b67e24 @nb Add small class for querying entries and its tests
nb authored
368 class WPCOM_Liveblog_Entries {
369
370 function __construct( $post_id, $key ) {
371 $this->post_id = $post_id;
372 $this->key = $key;
373 }
374
ad93bed @nb Query for comment_approved directly
nb authored
375 function get( $args = array() ) {
7b67e24 @nb Add small class for querying entries and its tests
nb authored
376 $defaults = array(
377 'post_id' => $this->post_id,
378 'orderby' => 'comment_date_gmt',
379 'order' => 'DESC',
380 'type' => $this->key,
ad93bed @nb Query for comment_approved directly
nb authored
381 'comment_approved' => $this->key,
7b67e24 @nb Add small class for querying entries and its tests
nb authored
382 );
383 $args = array_merge( $defaults, $args );
384 $entries = get_comments( $args );
385 return $entries;
386 }
387
388 function get_latest() {
389 $entries = $this->get( array( 'number' => 1 ) );
390 if ( empty( $entries ) )
391 return null;
392 return $entries[0];
393 }
394
395 function get_latest_timestamp() {
396 $latest = $this->get_latest();
397 if ( is_null( $latest ) ) {
398 return null;
399 }
8436590 @nb Use more generic function, instead of hacking own
nb authored
400 return mysql2date( 'G', $latest->comment_date_gmt );
7b67e24 @nb Add small class for querying entries and its tests
nb authored
401 }
4c6682f @nb Introduce get_between_timestamps()
nb authored
402
403 function get_between_timestamps( $start_timestamp, $end_timestamp ) {
404 $start_date = $this->mysql_from_timestamp( $start_timestamp );
405 $end_date = $this->mysql_from_timestamp( $end_timestamp );
406
8fa2583 @nb Filter entries for the between query on the application side
nb authored
407 $all_entries = $this->get();
408 $entries_between = array();
4c6682f @nb Introduce get_between_timestamps()
nb authored
409
8fa2583 @nb Filter entries for the between query on the application side
nb authored
410 foreach( $all_entries as $entry ) {
411 if ( $entry->comment_date_gmt >= $start_date && $entry->comment_date_gmt <= $end_date ) {
412 $entries_between[] = $entry;
413 }
414 }
415
416 return $entries_between;
4c6682f @nb Introduce get_between_timestamps()
nb authored
417 }
418
419 function add_between_conditions_for_where( $clauses, $query ) {
420 global $wpdb;
421 $vars = $query->query_vars;
422 $clauses['where'] = $wpdb->prepare( "( {$clauses['where']} ) AND ( comment_date_gmt BETWEEN %s AND %s )", $vars['start_date'], $vars['end_date'] );
423 return $clauses;
424 }
425
426 function add_comment_approved_condition_for_where( $clauses, $query ) {
427 global $wpdb;
428 $vars = $query->query_vars;
429 $clauses['where'] = $wpdb->prepare( "( {$clauses['where']} ) AND ( comment_approved = %s )", $this->key );
430 return $clauses;
431 }
432
433 private function mysql_from_timestamp( $timestamp ) {
434 return gmdate( 'Y-m-d H:i:s', $timestamp );
435 }
7b67e24 @nb Add small class for querying entries and its tests
nb authored
436 }
437
8ff4206 @mjangda v1
mjangda authored
438 WPCOM_Liveblog::load();
439 endif;
Something went wrong with that request. Please try again.