HEX
Server: Apache
System: Linux pdx1-shared-a1-38 6.6.104-grsec-jammy+ #3 SMP Tue Sep 16 00:28:11 UTC 2025 x86_64
User: mmickelson (3396398)
PHP: 8.1.31
Disabled: NONE
Upload Files
File: /home/mmickelson/martyknows.com/wp-content/themes/p2/inc/list-creator.php
<?php
/**
 * List Creator.
 *
 * Loosely based on a version of the doLists and related functions from the Markdown PHP Library
 * @see http://michelf.com/projects/php-markdown/
 *
 * Parses unordered and ordered lists on save (storing the HTML in the database).
 * If you switch themes, the list markup is preserved.
 *
 * Parses task lists on display: if you switch themes, you no
 * longer have a handler for checkbox state toggles, so just
 * show ASCII x's and o's.
 *
 * The checked state for each task list item is initially stored
 * as an x (checked) or o (unchecked) in the post/comment text.
 *
 * When a task list item is updated from the blog (via the HTML form),
 * the checked state is stored in post/comment meta.
 *
 * When the post/comment is edited, the checked states stored in
 * post/comment meta in copied to the post/comment text
 * (updating x's and o's) and deleted.
 *
 * This way, the order of the task list items can be
 * changed while editing the post/comment without breaking
 * the data stored in post/comment meta.
 *
 * @package P2
 */

/**
 * Parses lists for posts.
 *
 * @package P2
 */
class P2_Post_List_Creator extends P2_List_Creator {
	var $form_action_name = 'p2-post-task-list';

	function P2_Post_List_Creator() {
		parent::P2_List_Creator();

		// Parse everything on display
		add_filter( 'the_content', array( $this, 'reset_task_list_counter' ), 0 );
		add_filter( 'the_content', array( $this, 'reset_task_list_item_id' ), 0 );
		add_filter( 'the_content', array( $this, 'parse_list' ), 1 );

		// Renormalize task list meta into ASCII x's and o's
		add_filter( 'edit_post_content', array( $this, 'edit_post_content' ), 10, 2 );
		add_action( 'post_updated', array( $this, 'delete_all_item_data' ) );

		// Parse UL/OL on save
		add_filter( 'content_save_pre', array( $this, 'parse_list' ), 11 );
	}

	/**
	 * Returns ID of current/given post
	 *
	 * @param int $post_id (optional) post ID
	 * @return int post ID
	 */
	function get_object_id( $post_id = 0 ) {
		$post = get_post( $post_id );
		return (int) $post->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 P2_Comment_List_Creator() {
		parent::P2_List_Creator();

		// 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 P2_List_Creator() {
		// 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() {
?>
<style type="text/css">
.is-js .hide-if-js {
	display: none;
}
.p2-task-list ul {
	margin-left: 0 !important;
}
.p2-task-list ul ul {
	margin-left: 20px !important;
}
.p2-task-list li {
	list-style: none;
}
</style>
<?php
	}

	function js() {
?>
<script type="text/javascript">
jQuery( function( $ ) {
	$( 'body' )
		.addClass( 'is-js' )
		.delegate( '.p2-task-list :checkbox', 'click', function() {
			var $this = $( this ),
			    $li = $this.parents( 'li:first' ),
			    $form = $this.parents( 'form:first' ),
			    data = $li.find( ':input' ).serialize(),
			    colorEl = $li, origColor = $li.css( 'background-color' ), color;

			while ( colorEl.get(0).tagName && colorEl.css( 'background-color' ).match( /^\s*(rgba\s*\(\s*0+\s*,\s*0+\s*,\s*0+\s*,\s*0+\s*\)|transparent)\s*$/ ) ) {
				colorEl = colorEl.parent();
			}

			color = colorEl.get(0).tagName ? colorEl.css( 'background-color' ) : '#ffffff';

			data += '&ajax=1&' + $form.find( '.submit :input' ).serialize();

			$.post( $form.attr( 'action' ), data, function( response ) {
				if ( '1' === response )
					$li.css( 'background-color', '#F6F3D1' ).animate( { backgroundColor: color }, 'slow', function() { $li.css( 'background-color', origColor ); } );
			} );
	} );
} );
</script>
<?php
	}

	function regex_to_parse() {
		if ( $this->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 = '#\[(' . join( '|', 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, '<pre' ) ) {
			$text = preg_replace_callback( '#<pre(?:\s|>).*</pre>#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</$tag>\n\n";
			break;
		case 'x' : // Task List
		case 'o' : // Task List
			$return = "<ul>\n" . $this->process_task_list_items( $matches[0], $indent ) . "\n</ul>\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( '<form class="p2-task-list" id="p2-task-list-%d" action="%s" method="post">', $id, esc_url( $ajax_url ) ) . $return;
			$return .= "<p class='hide-if-js submit'>\n";
			$return .= "<input type='hidden' name='id' value='$id' />\n";
			$return .= sprintf( "<input type='hidden' name='action' value='%s' />\n", $this->form_action_name );
			$return .= "<input type='submit' value='Save' />\n";
			$return .= wp_nonce_field( "p2-task-list_$id", "_p2_task_list_nonce_$id", true, false );
			$return .= "\n</p>\n</form>";

			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[] = "<li>\n$item\n</li>";
			} else {
				$out[] = "<li>$item</li>";
			}
		}
		$text = join( "\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 = '<li id="p2-task-%1$d-%2$d"><label>%8$s<input type="checkbox" name="p2_task[%1$d][%2$d]"%3$s%4$s value="1" /><input type="hidden" name="p2_task_ids[%1$d][%2$d]" value="%6$s" /> %5$s%10$s%8$s</label></li>';
		}

		$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 = "<del{$check_time}>" . preg_replace( '/\n|\z/', '</del>$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 = join( "\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' );
	}
}