Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 369 lines (291 sloc) 12.093 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):
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 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
136 $filtered_entry->content = self::entry_output( $entry, false );
8ff4206 @mjangda 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 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
146 function entry_output( $entry, $echo = true ) {
8ff4206 @mjangda 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 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
156 $args = apply_filters( 'liveblog_entry_output_args', array(
8ff4206 @mjangda 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 @mjangda Load initial entries server-side rather than via AJAX for SEO benefit
mjangda authored
234 'last_timestamp' => self::get_last_entry_timestamp(),
8ff4206 @mjangda 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 @mjangda 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 @mjangda v1
mjangda authored
255 $liveblog_output = '';
256 $liveblog_output .= self::get_entry_editor_output();
cb778e4 @mjangda 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 @mjangda 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.