Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 369 lines (291 sloc) 12.093 kb
8ff4206 Mohammad Jangda 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):
11 -- Default styling
12 -- Max retries for ajax calls and delays
13
14 TODO (future):
15 -- Manual refresh button
16 -- Allow marking of liveblog as ended
17 -- Allow comment modifications; need to store modified date as comment_meta
18 -- Drag-and-drop image uploading support
19
20 */
21
22 if ( ! class_exists( 'WPCOM_Liveblog' ) ) :
23
24 class WPCOM_Liveblog {
25
26 const version = 0.1;
27 const key = 'liveblog';
28 const url_endpoint = 'liveblog';
29 const edit_cap = 'publish_posts';
30 const nonce_key = 'liveblog_nonce';
31
32 const refresh_interval = 5; // in seconds
33 const max_retries = 50;
34 const delay_threshold = 10;
35 const delay_multiplier = 1.5;
36
37 function load() {
38 add_action( 'init', array( __CLASS__, 'init' ) );
39 add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
40
41 add_action( 'wp_ajax_liveblog_insert_entry', array( __CLASS__, 'ajax_insert_entry' ) );
42 add_action( 'wp_ajax_liveblog_get_recent_entries', array( __CLASS__, 'ajax_get_recent_entries' ) );
43
44 add_filter( 'template_redirect', array( __CLASS__, 'handle_request' ) );
45
46 add_filter( 'comment_class', array( __CLASS__, 'add_comment_class' ) );
47
48 add_action( 'add_meta_boxes', array( __CLASS__, 'add_meta_box' ) );
49 add_action( 'save_post', array( __CLASS__, 'save_meta_box' ) );
50 }
51
52 function init() {
53 add_rewrite_endpoint( self::url_endpoint, EP_PERMALINK ); // /2012/01/01/post-name/liveblog/123456/ where 123456 is a timestamp
54
55 add_post_type_support( 'post', self::key );
56 }
57
58 function handle_request( $query ) {
59 // Check that current template is a liveblog
60 if ( ! self::is_viewing_liveblog_post() )
61 return;
62
63 // Check that this isn't a liveblog AJAX request
64 if ( self::is_liveblog_request() ) {
65 add_filter( 'the_content', array( __CLASS__, 'add_liveblog_to_content' ) );
66 return;
67 }
68
69 $timestamp = get_query_var( 'liveblog' );
70 $entries = self::ajax_get_recent_entries( $timestamp );
71 }
72 function is_liveblog_request() {
73 global $wp_query;
74 // Not using get_query_var since it returns '' for all requests, which is a valid for /post-name/liveblog/
75 return ! isset( $wp_query->query_vars['liveblog'] );
76 }
77 function ajax_insert_entry() {
78 self::_ajax_current_user_can_edit_liveblog();
79 self::_ajax_check_nonce();
80
81 $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
82
83 if ( ! $post_id )
84 self::json_return( false, __( 'Sorry, that\'s an invalid post_id', 'liveblog' ) );
85
86 $user = wp_get_current_user();
87
88 $entry_content = wp_filter_post_kses( $_POST['entry_content'] ); // these should have the same kses rules as posts
89
90 $entry = array(
91 'comment_post_ID' => $post_id,
92 'comment_content' => $entry_content,
93
94 'comment_approved' => self::key,
95 'comment_type' => self::key,
96
97 'user_id' => $user->ID,
98 // TODO: Should we be adding this or generating dynamically?
99 'comment_author' => $user->display_name,
100 'comment_author_email' => $user->user_email,
101 'comment_author_url' => $user->user_url,
102 // Borrowed from core as wp_insert_comment does not generate them
103 'comment_author_IP' => preg_replace( '/[^0-9a-fA-F:., ]/', '',$_SERVER['REMOTE_ADDR'] ),
104 'comment_agent' => substr( $_SERVER['HTTP_USER_AGENT'], 0, 254 ),
105 );
106
107 wp_insert_comment( $entry );
108
109 self::refresh_last_entry_timestamp();
110
111 self::json_return( true, '' );
112 }
113
114 function ajax_insert_image() {
115 $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
116 //
117 }
118
119 function ajax_get_recent_entries( $timestamp = 0 ) {
120 // When a new update is added, a cache key should be set/updated to the current GMT timestamp.
121 // The AJAX call can then just check that value and immediately abort if it knows that there is no
122 // new data to get yet without having to actually ask WordPress if there are new posts.
123
124 $last_timestamp = self::get_last_entry_timestamp();
125 if ( $last_timestamp && $timestamp >= $last_timestamp ) {
126 self::json_return( true, '', array( 'timestamp' => $last_timestamp ) );
127 }
128
129 $entries_output = array();
130 $entries = self::get_entries_since( get_the_ID(), $timestamp );
131 $filtered_entries = array();
132
133 foreach( $entries as $entry ) {
134 $filtered_entry = new stdClass;
135 $filtered_entry->ID = $entry->comment_ID;
cb778e4 Mohammad Jangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
136 $filtered_entry->content = self::entry_output( $entry, false );
8ff4206 Mohammad Jangda v1
mjangda authored
137 array_push( $filtered_entries, $filtered_entry );
138 }
139
140 if ( ! $last_timestamp )
141 self::refresh_last_entry_timestamp();
142
143 self::json_return( true, '', array( 'entries' => $filtered_entries, 'timestamp' => $last_timestamp ) );
144 }
145
cb778e4 Mohammad Jangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
146 function entry_output( $entry, $echo = true ) {
8ff4206 Mohammad Jangda v1
mjangda authored
147 $entry_id = $entry->comment_ID;
148 $post_id = $entry->comment_post_ID;
149 $output = '';
150
151 // Allow plugins to override the output
152 $output = apply_filters( 'liveblog_pre_entry_output', $output, $entry );
153 if ( $output )
154 return $output;
155
cb778e4 Mohammad Jangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
156 $args = apply_filters( 'liveblog_entry_output_args', array(
8ff4206 Mohammad Jangda v1
mjangda authored
157 'avatar_size' => 30,
158 ) );
159
160 $output .= '<div id="liveblog-entry-'. $entry_id .'" '. comment_class( '', $entry_id, $post_id, false ) . '>';
161 $output .= '<div class="liveblog-entry-text">';
162 $output .= get_comment_text( $entry_id );
163 $output .= '</div>';
164
165 $output .= '<header class="liveblog-meta">';
166 $output .= '<span class="liveblog-author-avatar">';
167 $output .= get_avatar( $entry->comment_author_email, $args['avatar_size'] );
168 $output .= '</span>';
169
170 $output .= '<span class="liveblog-author-name">'. get_comment_author_link( $entry_id ) .'</span>';
171 $output .= '<span class="liveblog-meta-time">';
172 $output .= '<a href="#liveblog-entry-'. $entry_id .'">';
173 $output .= sprintf( __('%1$s at %2$s'), get_comment_date( '', $entry_id ), get_comment_date( 'g:i a', $entry_id ) );
174 $output .= '</a>';
175 $output .= '</time>';
176 $output .= '</header>';
177 $output .= '</div>';
178
179 $output = apply_filters( 'liveblog_entry_output', $output, $entry );
180 if ( ! $echo )
181 return $output;
182 echo $output;
183 }
184
185 function add_comment_class( $classes ) {
186 $classes[] = 'liveblog-entry';
187 return $classes;
188 }
189
190 function get_entries_since( $post_id, $timestamp = 0 ) {
191 add_filter( 'comments_clauses', array( __CLASS__, 'comments_where_include_liveblog_status' ), false, 2 );
192 $entries = get_comments( array(
193 'post_id' => $post_id,
194 'orderby' => 'comment_date_gmt',
195 'order' => 'ASC',
196 ) );
197 remove_filter( 'comments_clauses', array( __CLASS__, 'comments_where_include_liveblog_status' ), false );
198
199 $filtered_entries = array();
200
201 foreach( $entries as $entry ) {
202 if ( $timestamp && strtotime( $entry->comment_date_gmt ) <= $timestamp )
203 continue;
204
205 $filtered_entries[ $entry->comment_ID ] = $entry;
206 }
207 $entries = $filtered_entries;
208
209 return $entries;
210 }
211
212 function comments_where_include_liveblog_status( $clauses, $query ) {
213 global $wpdb;
214 // TODO: should we only fetch the posts we want based on the timestamp in the current context?
215 $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 );
216 return $clauses;
217 }
218
219 function enqueue_scripts() {
220 if ( ! self::is_viewing_liveblog_post() )
221 return;
222
223 wp_enqueue_script( 'liveblog', plugins_url( 'js/liveblog.js', __FILE__ ), array( 'jquery' ), self::version, true );
224 wp_enqueue_style( 'liveblog', plugins_url( 'css/liveblog.css', __FILE__ ) );
225
226 if ( self::_current_user_can_edit_liveblog() )
227 wp_enqueue_script( 'liveblog-publisher', plugins_url( 'js/liveblog-publisher.js', __FILE__ ), array( 'liveblog' ), self::version, true );
228
229 $liveblog_settings = apply_filters( 'liveblog_settings', array(
230 'key' => self::key,
231 'nonce_key' => self::nonce_key,
232 'permalink' => get_permalink(),
233 'post_id' => get_the_ID(),
cb778e4 Mohammad Jangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
234 'last_timestamp' => self::get_last_entry_timestamp(),
8ff4206 Mohammad Jangda v1
mjangda authored
235
236 'refresh_interval' => self::refresh_interval,
237 'max_retries' => self::max_retries,
238 'delay_threshold' => self::delay_threshold,
239 'delay_multiplier' => self::delay_multiplier,
240
241 'ajaxurl' => admin_url( 'admin-ajax.php' ),
242 'entriesurl' => self::get_entries_endpoint_url(),
243
244 // i18n
245 'update_nag_singular' => __( '%d new update', 'liveblog' ), // TODO: is there a better way to do _n via js?
246 'update_nag_plural' => __( '%d new updates', 'liveblog' ),
247 ) );
248 wp_localize_script( 'liveblog', 'liveblog_settings', $liveblog_settings );
249 }
250
251 function add_liveblog_to_content( $content ) {
cb778e4 Mohammad Jangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
252 $post_id = get_the_ID();
253 $entries = self::get_entries_since( $post_id );
254
8ff4206 Mohammad Jangda v1
mjangda authored
255 $liveblog_output = '';
256 $liveblog_output .= self::get_entry_editor_output();
cb778e4 Mohammad Jangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
257 $liveblog_output .= '<div id="liveblog-entries" class="liveblog-container">';
258 foreach ( (array) $entries as $entry ) {
259 $liveblog_output .= self::entry_output( $entry, false );
260 }
261
262 $liveblog_output .= '</div>';
8ff4206 Mohammad Jangda v1
mjangda authored
263
264 return $content . $liveblog_output;
265 }
266
267 function get_entry_editor_output() {
268 if ( ! self::_current_user_can_edit_liveblog() )
269 return;
270
271 $editor_output = '';
272 $editor_output .= '<textarea id="liveblog-form-entry" name="liveblog-form-entry"></textarea>';
273 //$editor_output .= '<a href="#" id="liveblog-form-entry-submit" class="button">Submit</a>';
274 $editor_output .= '<input type="button" id="liveblog-form-entry-submit" class="button" value="'. __( 'Post Update' ) . '" />';
275 $editor_output .= wp_nonce_field( self::nonce_key, self::nonce_key, false, false );
276
277 return $editor_output;
278 }
279
280 function is_viewing_liveblog_post() {
281 return is_single() && self::is_liveblog_post();
282 }
283
284 function is_liveblog_post( $post_id = null ) {
285 if ( empty( $post_id ) ) {
286 global $post;
287 $post_id = $post->ID;
288 }
289 return get_post_meta( $post_id, self::key, true );
290 }
291
292 function add_meta_box( $post_type ) {
293 if ( post_type_supports( $post_type, self::key ) )
294 add_meta_box( self::key, __( 'Liveblog', 'liveblog' ), array( __CLASS__, 'display_meta_box' ) );
295 }
296 function display_meta_box( $post ) {
297 ?>
298 <label>
299 <input type="checkbox" name="is-liveblog" id="is-liveblog" value="1" <?php checked( self::is_liveblog_post( $post->ID ) ); ?> />
300 <?php esc_html_e( 'This is a liveblog', 'liveblog' ); ?>
301 </label>
302 <?php
303 wp_nonce_field( 'liveblog_nonce', 'liveblog_nonce', false );
304 }
305 function save_meta_box( $post_id ) {
306 if ( ! isset( $_POST['liveblog_nonce'] ) || ! wp_verify_nonce( $_POST['liveblog_nonce'], 'liveblog_nonce' ) )
307 return;
308
309 if ( isset( $_POST['is-liveblog'] ) )
310 update_post_meta( $post_id, self::key, 1 );
311 else
312 delete_post_meta( $post_id, self::key );
313 }
314
315
316 function refresh_last_entry_timestamp( $timestamp = null ) {
317 if ( ! $timestamp )
318 $timestamp = current_time( 'timestamp', 1 ); // always work against gmt
319
320 set_transient( 'liveblog-last-entry-timestamp', $timestamp );
321
322 return $timestamp;
323 }
324 function get_last_entry_timestamp() {
325 return get_transient( 'liveblog-last-entry-timestamp' );
326 }
327
328 function get_entries_endpoint_url( $post_id = 0, $timestamp = '' ) {
329 $url = trailingslashit( get_permalink( $post_id ) . self::url_endpoint );
330 if ( $timestamp )
331 $url .= trailingslashit( $timestamp );
332 return $url;
333 }
334
335 function _ajax_current_user_can_edit_liveblog() {
336 if ( ! self::_current_user_can_edit_liveblog() ) {
337 self::json_return( false, __( 'Cheatin\', uh?', 'liveblog' ) );
338 }
339 }
340 function _current_user_can_edit_liveblog() {
341 return current_user_can( apply_filters( 'liveblog_edit_cap', self::edit_cap ) );
342 }
343
344 function _ajax_check_nonce( $action = 'liveblog_nonce' ) {
345 if ( ! isset( $_REQUEST[ self::nonce_key ] ) || ! wp_verify_nonce( $_REQUEST[ self::nonce_key ], $action ) ) {
346 self::json_return( false, __( 'Sorry, we could not authenticate you.', 'liveblog' ) );
347 }
348 }
349
350 function json_return( $success, $message, $data = array(), $echo_and_exit = true ) {
351 $return = json_encode( array(
352 'status' => intval( $success ),
353 'message' => $message,
354 'data' => $data,
355 ) );
356
357 if ( $echo_and_exit ) {
358 echo $return;
359 exit;
360 } else {
361 return $return;
362 }
363 }
364
365 }
366
367 WPCOM_Liveblog::load();
368 endif;
Something went wrong with that request. Please try again.