_x('Draft', 'Post state/status', 'anno'),
'submitted' => _x('Submitted', 'Post state/status', 'anno'),
'in_review' => _x('In Review', 'Post state/status', 'anno'),
'approved' => _x('Approved', 'Post state/status', 'anno'),
'rejected' => _x('Rejected', 'Post state/status', 'anno'),
'published' => _x('Published', 'Post state/status', 'anno'),
);
/**
* Remove/Add meta boxes
*/
function annowf_meta_boxes() {
global $post;
$post_state = annowf_get_post_state($post->ID);
// Remove the WP Publish box
remove_meta_box('submitdiv', 'article', 'side');
// Remove author box
remove_meta_box('authordiv', 'article', 'normal');
// Remove slug div
remove_meta_box('slugdiv', 'article', 'normal');
if (!anno_user_can('manage_public_comments', null, $post->ID)) {
remove_meta_box('commentsdiv', 'article', 'normal');
}
// Remove Revisions box in favor of audit log box
//remove_meta_box('revisionsdiv', 'article', 'normal');
if (anno_user_can('view_audit')) {
add_meta_box('audit_log', _x('Audit Log', 'Meta box title', 'anno'), 'annowf_audit_log', 'article', 'normal', 'low');
}
// Remove taxonomy/edit boxes when a user is unable to save/edit
if (!anno_user_can('edit_post', null, $post->ID)) {
remove_meta_box('tagsdiv-article_tag', 'article', 'side');
remove_meta_box('article_category_select', 'article', 'side');
remove_meta_box('postimagediv', 'article', 'side', 'low');
}
if ( !(current_user_can('editor') || current_user_can('administrator') ) ) {
remove_meta_box('commentstatusdiv', 'article', 'normal');
}
// Custom author select box. Only displays co-authors in the dropdown.
add_meta_box('authordiv', _x('Author', 'Meta box title', 'anno'), 'annowf_author_meta_box', 'article', 'side', 'low');
// Add the Annotum workflow publish box.
global $annowf_states;
add_meta_box('submitdiv', _x('Status:', 'Meta box title', 'anno').' '. esc_html($annowf_states[$post_state]), 'annowf_status_meta_box', 'article', 'side', 'high');
// Clone data meta box. Only display if something has been cloned from this post, or it is a clone itself.
$posts_cloned = get_post_meta($post->ID, '_anno_posts_cloned', true);
$cloned_from = get_post_meta($post->ID, '_anno_cloned_from', true);
if (!empty($posts_cloned) || !empty($cloned_from)) {
add_meta_box('anno-cloned', _x('Versions', 'Meta box title', 'anno'), 'annowf_cloned_meta_box', 'article', 'side', 'low');
}
if (anno_user_can('view_reviewers')) {
add_meta_box('anno-reviewers', _x('Reviewers', 'Meta box title', 'anno'), 'annowf_reviewers_meta_box', 'article', 'side', 'low');
}
add_meta_box('anno-co-authors', _x('Co-Authors', 'Meta box title', 'anno'), 'annowf_co_authors_meta_box', 'article', 'side', 'low');
}
add_action('add_meta_boxes_article', 'annowf_meta_boxes');
/**
* Enqueue css for workflow
*/
function annowf_css() {
wp_enqueue_style('anno-workflow', trailingslashit(get_bloginfo('template_directory')).'plugins/workflow/css/workflow.css');
}
add_action('admin_print_styles-post-new.php', 'annowf_css');
add_action('admin_print_styles-post.php', 'annowf_css');
/**
* Enqueue js for internal comments
*/
function annowf_js() {
wp_enqueue_script('suggest');
wp_enqueue_script('anno-workflow', trailingslashit(get_bloginfo('template_directory')).'plugins/workflow/js/workflow.js', array('jquery', 'suggest'));
// Remove Auto-Save feature if a user cannot edit a post. *Note this prevents previewing inputted markup
if (!anno_user_can('edit_post')) {
wp_dequeue_script('autosave');
}
}
add_action('admin_print_scripts-post-new.php', 'annowf_js');
add_action('admin_print_scripts-post.php', 'annowf_js');
/**
* Article Save action for correcting WP status updating. Fires before insertion into DB
*/
function annowf_insert_post_data($post, $postarr) {
if ($post['post_type'] == 'article' && $post['post_status'] != 'auto-draft') {
global $anno_post_save;
if (isset($_POST['revert']) && $_POST['revert'] == $anno_post_save['revert']) {
$post['post_status'] = 'draft';
}
else if (isset($_POST['publish'])) {
switch ($_POST['publish']) {
case $anno_post_save['publish']:
$post['post_status'] = 'publish';
break;
case $anno_post_save['reject']:
case $anno_post_save['revisions']:
$post['post_status'] = 'draft';
break;
case $anno_post_save['approve']:
case $anno_post_save['review']:
$post['post_status'] = 'pending';
break;
default:
break;
}
}
}
return $post;
}
add_filter('wp_insert_post_data', 'annowf_insert_post_data', 10, 2);
/**
* Article state transistioning. Fires after post has been inserted into the database
*/
function annowf_transistion_state($post_id, $post, $post_before) {
if ($post->post_type == 'article' && $post->post_status != 'auto-draft') {
global $anno_post_save;
$current_user = wp_get_current_user();
$old_state = get_post_meta($post->ID, '_post_state', true);
$new_state = $old_state;
if (empty($old_state)) {
$old_state = 'draft';
}
if (isset($_POST['revert']) && $_POST['revert'] == $anno_post_save['revert']) {
$new_state = 'draft';
}
else if (isset($_POST['publish'])) {
switch ($_POST['publish']) {
// Make sure theres the ability to revert posts if they get a state on accident.
case $anno_post_save['revert']:
$new_state = 'draft';
break;
case $anno_post_save['approve']:
// Ensure proper state transitions
if (in_array($old_state, array('submitted', 'in_review'))) {
$new_state = 'approved';
}
break;
case $anno_post_save['revisions']:
if (in_array($old_state, array('submitted', 'in_review'))) {
$new_state = 'draft';
}
break;
case $anno_post_save['reject']:
// Publishing staff can still reject even if editor approves
if (in_array($old_state, array('submitted', 'in_review', 'approved'))) {
$new_state = 'rejected';
}
break;
case $anno_post_save['publish']:
if ($old_state == 'approved') {
$new_state = 'published';
}
break;
case $anno_post_save['review']:
$reviewers = anno_get_reviewers($post->ID);
if (is_array($reviewers) && count($reviewers) && in_array($old_state, array('submitted', 'draft'))) {
$new_state = 'in_review';
}
else if ($old_state == 'draft') {
$new_state = 'submitted';
}
break;
default:
break;
}
}
$notification_type = $new_state;
if ($old_state == 'draft' && $new_state == 'in_review') {
$notification_type = 're_review';
}
// Send back for revisions
if ($new_state == 'draft' && !empty($old_state) && $old_state != 'draft') {
$round = annowf_get_round($post_id);
update_post_meta($post_id, '_round', intval($round) + 1);
$notification_type = 'revisions';
}
if ($new_state != $old_state) {
if (empty($new_state)) {
$new_state = 'draft';
}
update_post_meta($post->ID, '_post_state', $new_state);
do_action('anno_state_change', $new_state, $old_state);
// Save to audit log
annowf_save_audit_item($post->ID, $current_user->ID, 2, array($old_state, $new_state));
// Send notifications, but not for published to draft state
if (anno_workflow_enabled('notifications') && !($old_state == 'published' && $new_state == 'draft')) {
annowf_send_notification($notification_type, $post);
}
}
// Author has changed, add original author as co-author, remove new author from co-authors
if ($post->post_author !== $post_before->post_author) {
annowf_add_user_to_post('author', $post_before->post_author, $post->ID);
annowf_remove_user_from_post('author', $post->post_author, $post->ID);
if (anno_workflow_enabled('notifications')) {
annowf_send_notification('primary_author', $post, null, array(anno_user_email($post->post_author)));
}
}
}
}
add_action('post_updated', 'annowf_transistion_state', 10, 3);
/**
* Store revisions in the audit log.
*/
function anno_put_post_revision($rev_id) {
$current_user = wp_get_current_user();
$revision = get_post($rev_id);
if (!empty($revision)) {
$post = get_post($revision->post_parent);
}
if (!empty($post) && $post->post_type == 'article') {
annowf_save_audit_item($post->ID, $current_user->ID, 1, array($revision->ID));
}
}
add_action('_wp_put_post_revision', 'anno_put_post_revision');
function annowf_create_user_meta_markup($type) {
?>
search for an existing user', 'search for user link', 'anno'); ?>
ID)) { ?>
invite a new user', 'invite user link', 'anno'), add_query_arg( 'user-role', 'author', menu_page_url( Wpcom_Invite_Users::page_slug, false ) ), esc_attr('anno-show-create-' . $type ) ); ?>
ID);
foreach ($reviewers as $user_id) {
$user = get_userdata($user_id);
if ($user) {
annowf_user_li_markup($user, 'reviewer');
}
// This user was deleted or no longer exists, remove from post meta
else if ($user_id !== false) {
delete_post_meta($post->ID, '_anno_reviewer_'.$user_id);
}
}
?>
ID);
foreach ($co_authors as $user_id) {
// Prevent primary author from showing up in this list.
if ($user_id == $post->post_author) {
continue;
}
$user = get_userdata($user_id);
if ($user) {
annowf_user_li_markup($user, 'co_author');
}
// This user was deleted or no longer exists, remove from post meta
else if ($user_id !== false) {
delete_post_meta($post->ID, '_anno_author_'.$user_id);
}
}
?>
ID);
if (isset($anno_review_options[$review])) {
$extra = $anno_review_options[$review].' ';
}
}
$remove = ' ';
// If a user can manage this type of user, show the remove link.
if (anno_user_can('manage_'.$type_plural, null, $post_id)) {
$remove = ' '._x('remove', 'remove user link for admin screens', 'anno').'';
}
?>
user_email), $response['user']);
}
if ($post_state == 'submitted') {
update_post_meta($post_id, '_post_state', 'in_review');
if (anno_workflow_enabled('notifications')) {
$post = get_post($post_id);
annowf_send_notification('in_review', $post);
}
}
// If the reviewer is being re-added and has already left a review for this round
$round = annowf_get_round($post_id);
$review = get_user_attribute($response['user']->ID, '_'.$post_id.'_review_'.$round, true);
if (!empty($review)) {
$reviewed = get_post_meta($post_id, '_round_'.$round.'_reviewed', true);
$reviewed[] = $response['user']->ID;
update_post_meta($post_id, '_round_'.$round.'_reviewed', array_unique($reviewed));
// Used for incrementation of x of x reviewed
$response['increment'] = 1;
}
else {
$response['increment'] = 0;
}
//Add to the audit log
$current_user = wp_get_current_user();
annowf_save_audit_item($post_id, $current_user->ID, 8, array($response['user']->ID));
}
unset($response['user']);
echo json_encode($response);
die();
}
add_action('wp_ajax_anno-add-reviewer', 'annowf_add_reviewer');
/**
* Handles AJAX request for adding a co-author to a post.
*/
function annowf_add_co_author() {
$response = annowf_add_user('co_author');
if ($response['message'] == 'success') {
// Used for quick access when filtering posts on a post-author page
$post_id = absint($_POST['post_id']);
// Send email
if (anno_workflow_enabled('notifications')) {
$post = get_post($post_id);
annowf_send_notification('co_author_added', $post, '', array($response['user']->user_email), $response['user']);
}
// Add author to JSON for appending to author dropdown
$response['author'] = '';
//Add to the audit log
$current_user = wp_get_current_user();
annowf_save_audit_item($post_id, $current_user->ID, 6, array($response['user']->ID));
}
unset($response['user']);
echo json_encode($response);
die();
}
add_action('wp_ajax_anno-add-co-author', 'annowf_add_co_author');
/**
* AJAX handler for adding a user to a post with a given type
*
* @param string $type Type of user to add to the post (co_author, reviewer)
* @return array Array of data pertaining to user added and JSON data.
*/
function annowf_add_user($type) {
check_ajax_referer('anno_manage_'.$type, '_ajax_nonce-manage-'.$type);
$message = 'error';
$html = '';
if (isset($_POST['post_id']) && isset($_POST['user'])) {
$user_login = sanitize_text_field( trim( $_POST['user'] ) );
$user = get_user_by( 'login', $user_login );
// Only users of the current site can be be added as authors or reviewers
if ( is_object( $user ) && !is_user_member_of_blog( $user->ID ) )
unset( $user );
if (!empty($user)) {
$post = get_post($_POST['post_id']);
$co_authors = anno_get_authors(absint($_POST['post_id']));
$reviewers = anno_get_reviewers(absint($_POST['post_id']));
if ($type == 'reviewer') {
$type_string = _x('reviewer', 'noun describing user', 'anno');
}
else {
$type_string = _x('co-author', 'noun describing user', 'anno');
}
if ($post->post_author == $user->ID) {
$html = sprintf(_x('Cannot add author as a %s', 'Adding user error message for article meta box', 'anno'), $type_string);
}
else if (in_array($user->ID, $co_authors) ) {
$html = sprintf(_x('Cannot add %s as %s. User is already a co-author', 'Adding user error message for article meta box', 'anno'), $user->user_login, $type_string);
}
else if (in_array($user->ID, $reviewers)) {
$html = sprintf(_x('Cannot add %s as %s. User is already a reviewer', 'Adding user error message for article meta box', 'anno'), $user->user_login, $type_string);
}
else if (annowf_add_user_to_post($type, $user->ID, absint($_POST['post_id']))) {
$message = 'success';
ob_start();
annowf_user_li_markup($user, $type);
$html = ob_get_contents();
ob_end_clean();
}
}
else {
$html = sprintf(_x('User \'%s\' not found or not a member of your blog.', 'Adding user error message for article meta box', 'anno'), $user_login);
}
}
return array('message' => $message, 'html' => $html, 'user' => $user);
}
/**
* Handles AJAX request for remove a reviewer to a post.
*/
function annowf_remove_reviewer() {
$response = annowf_remove_user('reviewer');
$response['decrement'] = 0;
if ($response['message'] == 'success') {
$post_id = absint($_POST['post_id']);
$user_id = absint($_POST['user_id']);
// Send back to submitted state if we've removed all the reviewers
$reviewers = anno_get_reviewers($post_id);
if ( ! is_countable( $reviewers ) || count( $reviewers ) == 0 ) {
update_post_meta($post_id, '_post_state', 'submitted');
}
// Check if the user had already left a review and send back in response to update dom appropriately
$round = annowf_get_round($post_id);
$reviews = get_post_meta($post_id, '_round_'.$round.'_reviewed', true);
if (!is_array($reviews)) {
$reviews = array();
}
if (in_array($user_id, $reviews)) {
$key = array_search($user_id, $reviews);
unset($reviews[$key]);
update_post_meta($post_id, '_round_'.$round.'_reviewed', $reviews);
$response['decrement'] = 1;
}
//Add to the audit log
$current_user = wp_get_current_user();
annowf_save_audit_item($post_id, $current_user->ID, 9, array($user_id));
}
echo json_encode($response);
die();
}
add_action('wp_ajax_anno-remove-reviewer', 'annowf_remove_reviewer');
/**
* Handles AJAX request for remove a co-author to a post.
*/
function annowf_remove_co_author() {
$response = annowf_remove_user('co_author');
if ($response['message'] == 'success') {
if (isset($_POST['user_id'])) {
//Add to the audit log
$current_user = wp_get_current_user();
annowf_save_audit_item(absint($_POST['post_id']), $current_user->ID, 7, array(absint($_POST['user_id'])));
}
}
echo json_encode($response);
die();
}
add_action('wp_ajax_anno-remove-co_author', 'annowf_remove_co_author');
/**
* AJAX handler for removing users from a post for a given type
*/
function annowf_remove_user($type) {
$response = [];
check_ajax_referer('anno_manage_'.$type, '_ajax_nonce-manage-'.$type);
$response['message'] = 'error';
if (isset($_POST['user_id']) && isset($_POST['post_id'])) {
if (annowf_remove_user_from_post($type, absint($_POST['user_id']), absint($_POST['post_id']))) {
$response['message'] = 'success';
}
}
return $response;
}
/**
* Fetch the post state of a given post. If no post state is found
* it will be determined by the post status
*
* @param int $post_id The id to fetch the post stat for
* @return string Post state
*/
function annowf_get_post_state($post_id) {
$post_state = get_post_meta($post_id, '_post_state', true);
if (!$post_state) {
$post = get_post($post_id);
if ($post) {
switch ($post->post_status) {
case 'publish':
$post_state = 'published';
break;
case 'draft':
case 'pending':
default:
$post_state = 'draft';
break;
}
update_post_meta($post_id, '_post_state', $post_state);
}
}
return $post_state;
}
/**
* Return the round associated with a post. Rounds are determined by the number
* of times they have been in the draft state. (sent for revisions)
*
* @param int $post_id The id to fetch the post stat for
* @return int Round
*/
function annowf_get_round($post_id) {
$round = get_post_meta($post_id, '_round', true);
if (!$round) {
$round = 0;
}
return $round;
}
/**
* Typeahead user search AJAX handler
*/
function annowf_user_search() {
global $wpdb;
$s = sanitize_text_field( stripslashes( $_GET['q'] ) );
$s = trim( $s );
if ( strlen( $s ) < 2 )
die; // require 2 chars for matching
$s = sprintf( '*%s*', $s ); // Enable wildcard searching
$search_args = array(
'search' => $s,
'who' => 'authors',
'fields' => array(
'display_name',
'user_login',
),
);
$matching_users = get_users( $search_args );
$results = array();
foreach( $matching_users as $matching_user ) {
$results[] = $matching_user->user_login;
}
echo implode($results, "\n");
die;
}
add_action('wp_ajax_anno-user-search', 'annowf_user_search');
/**
* Metabox for posts that have been cloned from this post
*/
function annowf_cloned_meta_box($post) {
//TODO check for trash/deleted
$cloned_from = get_post_meta($post->ID, '_anno_cloned_from', true);
$cloned_from_post = get_post($cloned_from_post);
if (!$cloned_from_post) {
return;
}
?>
$current_user->ID,
'post_status' => 'draft',
'post_title' => sprintf(_x('Cloned: %s', 'Cloned article title prepend', 'anno'), $post->post_title),
'post_content' => $post->post_content,
'post_excerpt' => $post->post_excerpt,
'post_type' => $post->post_type,
'post_parent' => $post->post_parent,
);
$new_id = wp_insert_post($new_post);
// Add to clone/cloned post meta
if ($new_id) {
$posts_cloned = get_post_meta($orig_id, '_anno_posts_cloned', true);
if (!is_array($posts_cloned)) {
$posts_cloned = array($new_id);
}
else {
$posts_cloned[] = $new_id;
}
update_post_meta($orig_id, '_anno_posts_cloned', $posts_cloned);
update_post_meta($new_id, '_anno_cloned_from', $orig_id);
}
return $new_id;
}
/**
* Custom meta Box For Author select.
*/
function annowf_author_meta_box($post) {
if (!anno_user_can('select_author')) {
$author = get_userdata($post->post_author);
echo esc_html($author->user_login);
}
else {
$authors = anno_get_authors($post->ID);
?>
implode(',', $authors),
'name' => 'post_author_override',
'selected' => empty($post->ID) ? $user_ID : $post->post_author,
'include_selected' => true
));
}
}
/**
* Admin request handler. Handles backend permission enforcement, cloning.
*/
function annowf_admin_request_handler() {
global $anno_post_save;
// Cloning. This must come before the enforcing of capabilities below.
if (isset($_POST['publish']) && $_POST['publish'] == $anno_post_save['clone']) {
if (!anno_user_can('clone_post')) {
wp_die(_x('You are not allowed to clone this post.', 'Cloned article error message', 'anno'));
}
$post_id = anno_get_post_id();
$new_id = annowf_clone_post($post_id);
if (!empty($new_id)) {
$url = add_query_arg('message', 11, get_edit_post_link($new_id, 'url'));
}
else {
$url = add_query_arg('message', 12, get_edit_post_link($post_id, 'url'));
}
wp_redirect($url);
die();
}
// Enforce Capabilities on the backend. Determine the action, and its relevant annotum capability
if (isset($_POST['action'])) {
$wp_action = $_POST['action'];
}
else if (isset($_GET['action'])) {
$wp_action = $_GET['action'];
}
if (isset( $_POST['deletepost'])) {
$wp_action = 'delete';
}
if (isset($_POST['post_type'])) {
$post_type = $_POST['post_type'];
}
else if (isset($_GET['post_type'])) {
$post_type = $_GET['post_type'];
}
else if (isset($_GET['revision'])) {
// We only get revision when restoring a given revision
$rev_id = $_GET['revision'];
$rev = get_post($rev_id);
if (isset($rev->post_parent)) {
$post = get_post($rev->post_parent);
if (isset($post->post_type)) {
$post_type = $post->post_type;
}
}
}
else {
$post = get_post(anno_get_post_id());
if (isset($post->post_type)) {
$post_type = $post->post_type;
}
}
if (!empty($wp_action) && !empty($post_type) && $post_type == 'article') {
switch ($wp_action) {
case 'postajaxpost':
case 'post':
case 'post-quickpress-publish':
case 'post-quickpress-save':
$anno_cap = 'edit_post';
break;
// Creation, editing, restoring from revision
case 'editpost':
case 'editattachment':
case 'autosave':
case 'restore':
case 'inline-save':
$anno_cap = 'edit_post';
break;
// For Viewing post-edit screen
case 'edit':
$anno_cap = 'view_post';
break;
case 'trash':
case 'untrash':
$anno_cap = 'trash_post';
break;
case 'delete':
$anno_cap = 'admin';
break;
default:
break;
}
if (!empty($anno_cap) && !anno_user_can($anno_cap)) {
add_filter('user_has_cap', 'annowf_user_has_cap_filter');
}
}
}
add_action('admin_init', 'annowf_admin_request_handler', 0);
/**
* Filter to remove WP caps from a user for a given action if they do not have the workflow caps
*/
function annowf_user_has_cap_filter($user_caps) {
// Remove all capabilities so the user cannot perform the current action.
return false;
}
/**
* Adjust the edit display to override default WP implementation
*/
function annowf_get_sample_permalink_html($return, $id, $new_title, $new_slug) {
$post = get_post($id);
if (!empty($post) && $post->post_type == 'article') {
if (anno_user_can('edit_slug', null, $id)) {
return $return;
}
else {
return '';
}
}
return $return;
}
add_filter('get_sample_permalink_html', 'annowf_get_sample_permalink_html', 10, 4);
/**
* If this article was imported, show a notice to the user, flag is removed on save.
*
*
*/
function annowf_imported_admin_notices($empty) {
global $pagenow;
if ($pagenow == 'post.php') {
global $post;
$imported = get_post_meta($post->ID, '_anno_imported', true);
if ($imported == '1') {
// Display notice
echo '
'.__('You are editing an imported article. Updating this article will likely change its XML and HTML output.').'
';
}
}
}
//add_action('admin_notices', 'annowf_imported_admin_notices')
/**
* Remove actions TD content if user cannot edit this post in the Workflow context
*
* @todo Preferably a non-js way to do this. Currently js is the only option
*/
function annowf_revision_remove_restore_link() {
// This is actually the original post, not the revision
global $post;
if ($post->post_type == 'article' && !anno_user_can('edit_post')) {
?>