ID; } /** * Determines if the current user has permission to edit the current/given post * * @param int $post_id (optional) post ID * @return bool */ function current_user_can( $post_id = 0 ) { return current_user_can( 'edit_post', $this->get_object_id( $post_id ) ); } /** * Whether we should look for and parse task lists * * @return bool */ function parse_task_lists() { return 'content_save_pre' != current_filter(); } /** * Gets the (meta) checked state for the given task list item on the current/given post. * * @param int $task_id * @param int $post_id (optional) * @return array ( checked, checked_by_user_id, checked_timestamp ) */ function get_item_data( $task_id, $post_id = 0 ) { $meta = get_post_meta( $this->get_object_id( $post_id ), "p2_task_{$task_id}", true ); if ( !$meta ) { return array(); } return explode( ':', $meta ); } /** * Sets the (meta) checked state for the given task list item on the current/given post * * @param int $task_id * @param bool $done * @param int $post_id (optional) */ function put_item_data( $task_id, $done = true, $post_id = 0 ) { update_post_meta( $this->get_object_id( $post_id ), "p2_task_{$task_id}", sprintf( '%d:%d:%s', $done, get_current_user_id(), time() ) ); } /** * Deletes the (meta) checked state for the given task list item on the current/given post. * The x/o checked state stored in post_content is not changed * * @param int $task_id * @param int $post_id (optional) */ function delete_item_data( $task_id, $post_id = 0 ) { delete_post_meta( $this->get_object_id( $post_id ), "p2_task_{$task_id}" ); } /** * Gets the post meta keys for each (meta) checked state for all task list items in the current/given post. * * @param int $post_id (optional) * @return array */ function get_all_item_data( $post_id = 0 ) { $meta_keys = get_post_custom_keys( $this->get_object_id( $post_id ) ); if ( !$meta_keys ) { return array(); } $task_id_meta_keys = preg_grep( '/p2_task_\d/', $meta_keys ); if ( !$task_id_meta_keys ) { return array(); } return $task_id_meta_keys; } /** * Deletes all (meta) checked states for the current/given post. * * @param int $post_id (optional) */ function delete_all_item_data( $post_id = 0 ) { $post_id = $this->get_object_id( $post_id ); $task_id_meta_keys = $this->get_all_item_data( $post_id ); foreach ( $task_id_meta_keys as $task_id_meta_key ) { delete_post_meta( $post_id, $task_id_meta_key ); } } /** * Wrapper for renormalizing task list meta into ASCII x's and o's during post edit. * Copies the post meta checked state to x's and o's in post_content */ function edit_post_content( $text, $post_id ) { $task_id_meta_keys = $this->get_all_item_data( $post_id ); if ( !$task_id_meta_keys ) { return $text; } $old_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : null; $GLOBALS['post'] = get_post( $post_id ); $text = $this->unparse_list( $text ); $GLOBALS['post'] = $old_post; return $text; } } /** * Parses lists for comments. * * @package P2 */ class P2_Comment_List_Creator extends P2_List_Creator { var $form_action_name = 'p2-comment-task-list'; function __construct() { parent::__construct(); // Parse everything on display add_filter( 'comment_text', array( $this, 'reset_task_list_counter' ), 0 ); add_filter( 'comment_text', array( $this, 'reset_task_list_item_id' ), 0 ); add_filter( 'comment_text', array( $this, 'comment_text' ), 11, 2 ); // Renormalize task list meta into ASCII x's and o's add_filter( 'p2_get_comment_content', array( $this, 'unparse_comment_list' ), 10, 2 ); add_action( 'edit_comment', array( $this, 'delete_all_item_data' ) ); // Parse UL/OL on save add_filter( 'pre_comment_content', array( $this, 'parse_list' ) ); } /** * Parses all lists on display * * Fires on 'comment_text' * * @param string $comment_text * @param object $comment Comment row object * @return string */ function comment_text( $comment_text, $comment = null ) { $old_comment = isset( $GLOBALS['comment'] ) ? $GLOBALS['comment'] : null; $GLOBALS['comment'] = $comment; $comment_text = $this->parse_list( $comment_text ); $GLOBALS['comment'] = $old_comment; return $comment_text; } /** * Returns ID of current/given comment * * @param int $comment_id (optional) comment ID * @return int comment ID */ function get_object_id( $comment_id = 0 ) { $comment = get_comment( $comment_id ); return is_object( $comment ) ? $comment->comment_ID : 0; } /** * Determines if the current user has permission to edit the current/given comment * * @param int $comment_id (optional) comment ID * @return bool */ function current_user_can( $comment_id = 0 ) { return current_user_can( 'edit_comment', $this->get_object_id( $comment_id ) ); } /** * Whether we should look for and parse task lists * * @return bool */ function parse_task_lists() { return 'pre_comment_content' != current_filter(); } /** * Gets the (meta) checked state for the given task list item on the current/given comment. * * @param int $task_id * @param int $comment_id (optional) * @return array ( checked, checked_by_user_id, checked_timestamp ) */ function get_item_data( $task_id, $comment_id = 0 ) { $meta = get_comment_meta( $this->get_object_id( $comment_id ), "p2_task_{$task_id}", true ); if ( !$meta ) { return array(); } return explode( ':', $meta ); } /** * Sets the (meta) checked state for the given task list item on the current/given comment * * @param int $task_id * @param bool $done * @param int $comment_id (optional) */ function put_item_data( $task_id, $done = true, $comment_id = 0 ) { update_comment_meta( $this->get_object_id( $comment_id ), "p2_task_{$task_id}", sprintf( '%d:%d:%s', $done, get_current_user_id(), time() ) ); } /** * Deletes the (meta) checked state for the given task list item on the current/given comment. * The x/o checked state stored in comment_content is not changed * * @param int $task_id * @param int $comment_id (optional) */ function delete_item_data( $task_id, $comment_id = 0 ) { delete_comment_meta( $this->get_object_id( $comment_id ), "p2_task_{$task_id}" ); } /** * Gets the comment meta keys for each (meta) checked state for all task list items in the current/given comment. * * @param int $comment_id (optional) * @return array */ function get_all_item_data( $comment_id = 0 ) { $comment_id = $this->get_object_id( $comment_id ); $meta = get_metadata( 'comment', $comment_id ); if ( !$meta ) { return array(); } $meta_keys = array_keys( $meta ); if ( !$meta_keys ) { return array(); } $task_id_meta_keys = preg_grep( '/p2_task_\d/', $meta_keys ); if ( !$task_id_meta_keys ) { return array(); } return $task_id_meta_keys; } /** * Deletes all (meta) checked states for the current/given comment. * * @param int $comment_id (optional) */ function delete_all_item_data( $comment_id = 0 ) { $comment_id = $this->get_object_id( $comment_id ); $task_id_meta_keys = $this->get_all_item_data( $comment_id ); foreach ( $task_id_meta_keys as $task_id_meta_key ) { delete_comment_meta( $comment_id, $task_id_meta_key ); } } /** * Wrapper for renormalizing task list meta into ASCII x's and o's during comment edit. * Copies the comment meta checked state to x's and o's in comment_content */ function unparse_comment_list( $text, $comment_id ) { if ( !$this->get_all_item_data( $comment_id ) ) { return $text; } $old_comment = isset( $GLOBALS['comment'] ) ? $GLOBALS['comment'] : null; $GLOBALS['comment'] = get_comment( $comment_id ); $text = $this->unparse_list( $text ); $GLOBALS['comment'] = $old_comment; return $text; } } /** * Central class for parsing lists. * * @package P2 */ class P2_List_Creator { /** * @var string name for action parameter of HTML form (not the action attribute, which is always the admin-ajax.php URL) */ var $form_action_name = ''; /** * @var bool Are we currently in a nested list? */ var $doing_recursion = false; var $preserved_texts = array(); function __construct() { // Have we done the CSS/JS already? static $did_header = false; if ( $this->form_action_name ) { // Add form submission handler add_action( "wp_ajax_{$this->form_action_name}", array( $this, 'submit' ) ); } if ( $did_header ) { return; } $did_header = true; add_action( 'wp_head', array( $this, 'css' ) ); add_action( 'wp_head', array( $this, 'js' ) ); if ( !is_admin() ) { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_js' ) ); } } function enqueue_js() { wp_enqueue_script( 'jquery-color' ); } function css() { ?> parse_task_lists() ) { // Parse UL/OL/Task lists return '! # $0 = whole list ^ ([ ]{0,3}) # $1 = nested list space padding ( # $2 = list item marker ([xo]) # $3 = task list item marker | [#*-] # UL/OL item marker ) \s+ # Mandatory whitespace after the list item marker .* # List item $ # EOL (?: # Multiple list items of the same type \n # New line ^ # BOL (?: \1 # Same amount of padding (?(3)(?3)|\2) # Same list item marker | # OR ... \1[ ]{1,} # Increased padding (start of nested list) (?2) # Any list item marker ) \s+ # Mandatory whitespace after the list item marker .* # List item $ # EOL )* !mx'; } return '! # $0 = whole list ^ ([ ]{0,3}) # $1 = number of spaces ([#*-]) # $2 = UL/OL item marker \s+ # Mandatory whitespace after the list item marker .* # List item $ # EOL (?: \n # New line ^ # BOL \1[ ]* # Same or increased amount of padding [xo#*-] # Any list item marker \s+ # Mandatory whitespace after the list item marker .* # List item $ # EOL )* !mx'; } function task_list_regex_to_unparse() { return '! # $0 = whole list ^ ([ ]{0,3}) # $1 = number of spaces ([xo]) # $2 = tasklist item marker \s+ # Mandatory whitespace after the list item marker .* # List item $ # EOL (?: \n # New line ^ # BOL \1[ ]* # Same or increased amount of padding [xo#*-] # Any list item marker \s+ # Mandatory whitespace after the list item marker .* # List item $ # EOL )* !mx'; } function preserve_text( $text ) { global $SyntaxHighlighter; if ( false !== strpos( $text, '[' ) && is_a( $SyntaxHighlighter, 'SyntaxHighlighter' ) && $SyntaxHighlighter->shortcodes ) { $shortcodes_regex = '#\[(' . implode( '|', array_map( 'preg_quote', $SyntaxHighlighter->shortcodes ) ) . ')(?:\s|\]).*\[/\\1\]#s'; $text = preg_replace_callback( $shortcodes_regex, array( $this, 'preserve_text_callback' ), $text ); } if ( false !== strpos( $text, ').*#s', array( $this, 'preserve_text_callback' ), $text ); } return $text; } function preserve_text_callback( $matches ) { $hash = md5( $matches[0] ); $this->preserved_text[$hash] = $matches[0]; return "[preserved_text $hash /]"; } function restore_text( $text ) { if ( false === strpos( $text, '[preserved_text ' ) ) { return $text; } return preg_replace_callback( '#\[preserved_text (\S+) /\]#', array( $this, 'restore_text_callback' ), $text ); } function restore_text_callback( $matches ) { if ( isset( $this->preserved_text[$matches[1]] ) ) { return $this->preserved_text[$matches[1]]; } return $matches[0]; } /** * Converts * and - into ULs, # into OLs, and x and o into task lists. * * @param string $text Plaintext to parse for lists * @param bool $doing_recursion Are we in a nested list? * @return string HTML */ function parse_list( $text, $doing_recursion = false ) { $text = $this->preserve_text( $text ); $text = preg_replace( '/(\r\n|\r|\n)/', "\n", $text ); // Run our regex through the callback, get the eventual text a few levels down and return it back to P2 here. $old_doing_recursion = $this->doing_recursion; $this->doing_recursion = $doing_recursion; $r = preg_replace_callback( $this->regex_to_parse(), array( $this, '_do_list_callback' ), $text ); $this->doing_recursion = $old_doing_recursion; return $this->restore_text( $r ); } function task_list_counter( $action = 'get' ) { static $id = 0; switch ( $action ) { case 'increment' : $id++; break; case 'reset' : $id = 0; break; } return $id; } function reset_task_list_counter( $content ) { $this->task_list_counter( 'reset' ); return $content; } function task_list_item_id( $action = 'get' ) { static $item_ids = array(); $object_id = $this->get_object_id(); if ( !isset( $item_ids[$object_id] ) ) { $item_ids[$object_id] = 0; } switch ( $action ) { case 'increment' : $item_ids[$object_id]++; break; case 'reset' : $item_ids[$object_id] = 0; break; } return $item_ids[$object_id]; } function reset_task_list_item_id( $content ) { $this->task_list_item_id( 'reset' ); return $content; } /** * Adds UL/OL markup, adds FORM markup for task lists. Calls internal functions for adding LI markup. * * @param array $matches Regex matches from ::parse_list() * @return string HTML */ function _do_list_callback( $matches ) { $id = $this->task_list_counter(); $doing_recursion = $this->doing_recursion; $indent = strlen( $matches[1] ); switch ( $matches[2] ) { case '*' : // UL case '-' : // UL case '#' : // OL if ( '#' == $matches[2] ) { $tag = 'ol'; } else { $tag = 'ul'; } // Easy peasy, lemon squeezy. return "<$tag>\n" . $this->process_list_items( $matches[0], $indent, $matches[2] ) . "\n\n\n"; break; case 'x' : // Task List case 'o' : // Task List $return = "\n\n"; if ( !$this->current_user_can( $this->get_object_id() ) ) { // User is not allowed to edit the post/comment. No form required. return $return; } // Don't nest form elements if ( $doing_recursion ) { return $return; } $id = $this->task_list_counter( 'increment' ); // Add form $ajax_url = remove_query_arg( 'p2ajax', P2_JS::ajax_url() ); $return = sprintf( '
', $id, esc_url( $ajax_url ) ) . $return; $return .= "

\n"; $return .= "\n"; $return .= sprintf( "\n", $this->form_action_name ); $return .= "\n"; $return .= wp_nonce_field( "p2-task-list_$id", "_p2_task_list_nonce_$id", true, false ); $return .= "\n

\n
"; return $return; } } /** * Adds LI markup. Recursively calls ::parse_list() to handle nested lists. * * @param string $text Plaintext list items * @param int $indent Number of padding spacess (nesting level) * @param string $marker Which list item marker is being processed (#, *, -) * @return string HTML */ function process_list_items( $text, $indent, $marker ) { // Break list into list items with the same nesting level and item marker $items = array_map( 'trim', preg_split( '/^[ ]{' . $indent . '}[' . $marker . ']/m', $text, -1, PREG_SPLIT_NO_EMPTY ) ); $out = array(); foreach ( $items as $item ) { if ( false !== strpos( $item, "\n" ) ) { // Has a nested list. Newlines for parseability in recursion $out[] = "
  • \n$item\n
  • "; } else { $out[] = "
  • $item
  • "; } } $text = implode( "\n", $out ); return $this->parse_list( $text, true ); } /** * Adds LI markup, adds INPUT markup. Recursively calls ::parse_list() to handle nested lists. * * @param string $text Plaintext list items * @param int $indent Number of padding spacess (nesting level) * @param string $context 'display' or 'edit' * @return string HTML */ function process_task_list_items( $text, $indent, $context = 'display' ) { global $post, $comment; $object_id = $this->get_object_id(); $current_user_can = $this->current_user_can(); $item_id = $this->task_list_item_id(); // Break list into list items with the same nesting level and note item marker (x, o) $items = array_map( 'trim', preg_split( '/^[ ]{' . $indent . '}([xo])/m', $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE ) ); // Heinous sprintf if ( 'edit' == $context ) { $format = '%9$s%7$s %5$s%10$s'; } else { $format = '
  • '; } $out = array(); foreach ( $items as $i => $item ) { if ( !( $i % 2 ) ) { // Item marker: x, o $checked_in_post_state = $item; $checked_in_post = 'x' == $checked_in_post_state; continue; } $item_id = $this->task_list_item_id( 'increment' ); $checked_in_meta = $this->get_item_data( $item_id ); if ( $checked_in_meta ) { list( $checked, $checker, $check_timestamp ) = $checked_in_meta; if ( $checked ) { $user = get_user_by( 'id', $checker ); $task_meta = " (@{$user->user_login})"; $check_time = ' datetime="' . esc_attr( gmdate( 'Y-m-d\TH:i:s+0000', $check_timestamp ) ) . '"'; } else { $task_meta = ''; $check_time = ''; } } else { $checked = $checked_in_post; $task_meta = ''; $check_time = ''; } $disabled = $current_user_can ? '' : ' disabled="disabled"'; if ( 'edit' != $context ) { if ( $checked ) { $item = "" . preg_replace( '/\n|\z/', '$0', $item, 1 ); } } // Heinous sprintf $out[] = sprintf( $format, $object_id, $item_id, checked( $checked, true, false ), $disabled, $item, $checked_in_post_state, $checked ? 'X' : 'O', // uppercase to not cause infinite loop in recursion @see ::unparse_list() false === strpos( $item, "\n" ) ? '' : "\n", str_repeat( ' ', $indent ), $task_meta ); } $text = implode( "\n", $out ) . "\n"; if ( 'edit' == $context ) { return $this->unparse_list( $text ); } return $this->parse_list( $text, true ); } /** * Handles form submission (AJAX or traditional) */ function submit() { $id = (int) $_POST['id']; $is_ajax = isset( $_POST['ajax'] ) && $_POST['ajax']; if ( $is_ajax ) { check_ajax_referer( "p2-task-list_$id", "_p2_task_list_nonce_$id" ); } else { check_admin_referer( "p2-task-list_$id", "_p2_task_list_nonce_$id" ); } foreach ( $_POST['p2_task_ids'] as $object_id => $tasks ) { foreach ( $tasks as $task_id => $checked_in_post_state ) { $checked_now = isset( $_POST['p2_task'][$object_id][$task_id] ) && $_POST['p2_task'][$object_id][$task_id]; $checked_in_post = 'x' == $checked_in_post_state; $checked_in_meta = $this->get_item_data( $task_id, $object_id ); if ( $checked_in_meta ) { list( $checked ) = $checked_in_meta; } else { $checked = $checked_in_post; } if ( $checked_now == $checked ) { continue; } if ( $checked_now ) { $this->put_item_data( $task_id, true, $object_id ); } else { if ( $checked_in_post ) { $this->put_item_data( $task_id, false, $object_id ); } else { $this->delete_item_data( $task_id, $object_id ); } } } } // @todo send back new list item content with DEL tag, @mention, etc. if ( $is_ajax ) { die( '1' ); } wp_safe_redirect( wp_get_referer() ); exit; } /** * Renormalizes (meta) checked state to ASCII x's and o's * * @param string $text * @return string */ function unparse_list( $text ) { $text = preg_replace( '/(\r\n|\r|\n)/', "\n", $text ); $text = preg_replace_callback( $this->task_list_regex_to_unparse(), array( $this, '_fix_task_list_callback' ), $text ); return preg_replace_callback( '/^[ ]*[XO]/m', array( $this, '_strtolower' ), $text ); } function _strtolower( $matches ) { return strtolower( $matches[0] ); } function _fix_task_list_callback( $matches ) { $indent = strlen( $matches[1] ); return $this->process_task_list_items( $matches[0], $indent, 'edit' ); } }