attach_hooks();
self::remove_comment_type();
}
/**
* Attach all relevant actions to handle comments.
*
* @since 1.13.0
* @access private
*/
private function attach_hooks() {
add_action( 'wp_ajax_upstream:project.add_comment', array( self::$namespace, 'store_comment' ) );
add_action( 'wp_ajax_upstream:project.add_comment_reply', array( self::$namespace, 'store_comment_reply' ) );
add_action( 'wp_ajax_upstream:project.trash_comment', array( self::$namespace, 'trash_comment' ) );
add_action( 'wp_ajax_upstream:project.unapprove_comment', array( self::$namespace, 'unapprove_comment' ) );
add_action( 'wp_ajax_upstream:project.approve_comment', array( self::$namespace, 'approve_comment' ) );
add_action( 'wp_ajax_upstream:project.fetch_comments', array( self::$namespace, 'fetch_comments' ) );
add_filter( 'comment_notification_subject', array( self::$namespace, 'define_notification_header' ), 10, 2 );
add_filter( 'comment_notification_recipients', array( self::$namespace, 'define_notification_recipients' ), 10, 2 );
add_filter( 'comment_notification_text', array( self::$namespace, 'add_item_title_to_notification' ), 10, 2 );
add_filter( 'upstream_allowed_tags_in_comments', array( self::$namespace, 'filter_allowed_tags' ) );
add_filter(
'comment_notification_headers',
array( self::$namespace, 'filter_comment_notification_headers' ),
10,
2
);
add_filter( 'comment_notification_text', array( self::$namespace, 'filter_comment_notification_text' ), 10, 2 );
}
/**
* Empties the comment_type="comment" column from UpStream comments.
*
* @since 1.16.3
* @static
*/
public static function remove_comment_type() {
$did_remove_comments_type = (bool) get_option( 'upstream:remove_comments_type' );
if ( ! $did_remove_comments_type ) {
global $wpdb;
$wpdb->query(
sprintf(
'UPDATE `%s` AS `comment`
LEFT JOIN `%s` AS `post`
ON `post`.`ID` = `comment`.`comment_post_ID`
SET `comment_type` = ""
WHERE `comment_type` = "comment"
AND `post_type` = "project"',
$wpdb->prefix . 'comments',
$wpdb->prefix . 'posts'
)
);
update_option( 'upstream:remove_comments_type', 1 );
}
}
/**
* Filter allowed tags.
*
* @param array $allowed_tags Allowed tags.
*
* @return array
*/
public static function filter_allowed_tags( $allowed_tags ) {
global $allowedtags;
// Add default allowed tags.
$allowed_tags = array_merge( $allowed_tags, $allowedtags );
// Add basic tags.
if ( ! array_key_exists( 'p', $allowed_tags ) ) {
$allowed_tags['p'] = array(
'class' => true,
'id' => true,
);
}
if ( ! array_key_exists( 'br', $allowed_tags ) ) {
$allowed_tags['br'] = array();
}
if ( ! array_key_exists( 'strong', $allowed_tags ) ) {
$allowed_tags['strong'] = array(
'class' => true,
'id' => true,
);
}
if ( ! array_key_exists( 'em', $allowed_tags ) ) {
$allowed_tags['em'] = array(
'class' => true,
'id' => true,
);
}
if ( ! array_key_exists( 'span', $allowed_tags ) ) {
$allowed_tags['span'] = array(
'class' => true,
'id' => true,
'style' => true,
);
}
if ( ! array_key_exists( 'del', $allowed_tags ) ) {
$allowed_tags['del'] = array(
'class' => true,
'id' => true,
);
}
if ( ! array_key_exists( 'ul', $allowed_tags ) ) {
$allowed_tags['ul'] = array(
'class' => true,
'id' => true,
);
}
if ( ! array_key_exists( 'ol', $allowed_tags ) ) {
$allowed_tags['ol'] = array(
'class' => true,
'id' => true,
);
}
if ( ! array_key_exists( 'li', $allowed_tags ) ) {
$allowed_tags['li'] = array(
'class' => true,
'id' => true,
);
}
if ( ! array_key_exists( 'a', $allowed_tags ) ) {
$allowed_tags['a'] = array(
'class' => true,
'id' => true,
'href' => true,
'charset' => true,
'name' => true,
'rel' => true,
'target' => true,
'type' => true,
);
} else {
$allowed_tags['a']['class'] = true;
$allowed_tags['a']['id'] = true;
$allowed_tags['a']['href'] = true;
$allowed_tags['a']['charset'] = true;
$allowed_tags['a']['name'] = true;
$allowed_tags['a']['rel'] = true;
$allowed_tags['a']['target'] = true;
$allowed_tags['a']['type'] = true;
}
// If the current can't post images, we return current supported tags.
if ( ! current_user_can( 'upstream_comment_images' ) ) {
return $allowed_tags;
}
// The user can post images, so let's allow the img tag.
if ( ! is_array( $allowed_tags ) ) {
$allowed_tags = array();
}
$allowed_tags['img'] = array(
'class' => true,
'src' => true,
'alt' => true,
'width' => true,
'height' => true,
);
return $allowed_tags;
}
/**
* AJAX endpoint that stores a new comment.
*
* @throws \Exception Set the error message.
* @since 1.13.0
* @static
*/
public static function store_comment() {
header( 'Content-Type: application/json' );
$post_data = isset( $_POST ) ? wp_unslash( $_POST ) : array();
$response = array(
'success' => false,
'error' => null,
);
try {
// Check if the request payload is potentially invalid.
if (
! defined( 'DOING_AJAX' )
|| ! DOING_AJAX
|| empty( $post_data )
|| ! isset( $post_data['nonce'] )
|| ! isset( $post_data['project_id'] )
|| ! isset( $post_data['item_type'] )
|| ! self::is_item_type_valid( sanitize_text_field( $post_data['item_type'] ) )
|| ! isset( $post_data['content'] )
) {
throw new \Exception( __( 'Invalid request.', 'upstream' ) );
}
// Prepare data to verify nonce.
$comment_target_item_type = strtolower( sanitize_text_field( $post_data['item_type'] ) );
if ( 'project' !== $comment_target_item_type ) {
if (
! isset( $post_data['item_id'] )
|| empty( $post_data['item_id'] )
) {
throw new \Exception( __( 'Invalid item.', 'upstream' ) );
}
// non-numeric id.
$item_id = sanitize_text_field( $post_data['item_id'] );
$nonce_identifier = 'upstream:project.' . $comment_target_item_type . 's.add_comment';
} else {
$item_id = absint( $post_data['project_id'] );
$nonce_identifier = 'upstream:project.add_comment';
}
// Verify nonce.
if ( ! check_ajax_referer( $nonce_identifier, 'nonce', false ) ) {
throw new \Exception( __( 'Invalid nonce.', 'upstream' ) );
}
// Check if the project exists.
$project_id = absint( $post_data['project_id'] );
if ( $project_id <= 0 ) {
throw new \Exception( __( 'Invalid Project.', 'upstream' ) );
}
// Check if commenting is disabled on the given project.
if ( upstream_are_comments_disabled( $project_id ) ) {
throw new \Exception( __( 'Commenting is disabled on this project.', 'upstream' ) );
}
// Check if the user has enough permissions to insert a new comment.
if ( ! upstream_can_access_field( 'publish_project_discussion', $comment_target_item_type, $item_id, UPSTREAM_ITEM_TYPE_PROJECT, $project_id, 'comments', UPSTREAM_PERMISSIONS_ACTION_EDIT, true ) ) {
throw new \Exception( __( "You're not allowed to do this.", 'upstream' ) );
}
$user_id = get_current_user_id();
$comment_content = stripslashes( wp_kses_post( $post_data['content'] ) );
$item_title = isset( $post_data['item_title'] ) ? sanitize_text_field( $post_data['item_title'] ) : '';
$comment = new Comment( $comment_content, $project_id, $user_id );
$server = isset( $_SERVER ) ? wp_unslash( $_SERVER ) : array();
$comment->created_by->ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', sanitize_text_field( $server['REMOTE_ADDR'] ) );
$comment->created_by->agent = isset( $server['HTTP_USER_AGENT'] ) ? sanitize_text_field( $server['HTTP_USER_AGENT'] ) : null;
$comment->save();
update_comment_meta( $comment->id, 'type', $comment_target_item_type );
if ( 'project' !== $comment_target_item_type ) {
update_comment_meta( $comment->id, 'id', $item_id );
// We store the item title here because of the project's data structure.
// It is faster to retrieve from metadata then seek item by item from a project.
update_comment_meta( $comment->id, 'title', $item_title );
}
wp_new_comment_notify_moderator( $comment->id );
wp_notify_postauthor( $comment->id );
$use_admin_layout = ! isset( $post_data['teeny'] ) ? true : boolval( $post_data['teeny'] ) === false;
$response['comment_html'] = stripslashes( $comment->render( true, $use_admin_layout ) );
$response['success'] = true;
} catch ( \Exception $e ) {
$response['error'] = $e->getMessage();
}
wp_send_json( $response );
}
/**
* Check if the item type is valid.
*
* @since 1.13.0
* @static
*
* @param string $item_type Value to be validated.
* @throws \Exception Set error message.
*
* @return bool
*/
public static function is_item_type_valid( $item_type ) {
$item_types = array( 'project', 'milestone', 'task', 'bug', 'file' );
return in_array( $item_type, $item_types );
}
/**
* AJAX endpoint that adds a new comment reply.
*
* @throws \Exception Set the error message.
* @since 1.13.0
* @static
*/
public static function store_comment_reply() {
header( 'Content-Type: application/json' );
$post_data = isset( $_POST ) ? wp_unslash( $_POST ) : array();
$server = isset( $_SERVER ) ? wp_unslash( $_SERVER ) : array();
$response = array(
'success' => false,
'error' => null,
);
try {
// Check if the request payload is potentially invalid.
if (
! defined( 'DOING_AJAX' )
|| ! DOING_AJAX
|| empty( $post_data )
|| ! isset( $post_data['nonce'] )
|| ! isset( $post_data['project_id'] )
|| ! isset( $post_data['item_type'] )
|| ! self::is_item_type_valid( sanitize_text_field( $post_data['item_type'] ) )
|| ! isset( $post_data['content'] )
|| ! isset( $post_data['parent_id'] )
|| ! is_numeric( sanitize_text_field( $post_data['parent_id'] ) )
|| ! check_ajax_referer( 'upstream:project.add_comment_reply:' . sanitize_text_field( $post_data['parent_id'] ), 'nonce', false )
) {
throw new \Exception( __( 'Invalid request.', 'upstream' ) );
}
// could be alnum ID.
$item_id = sanitize_text_field( $post_data['item_id'] );
// Check if the project exists.
$project_id = absint( $post_data['project_id'] );
if ( $project_id <= 0 ) {
throw new \Exception( __( 'Invalid Project.', 'upstream' ) );
}
$comment_target_item_type = strtolower( sanitize_text_field( $post_data['item_type'] ) );
if ( 'project' !== $comment_target_item_type ) {
if (
! isset( $post_data['item_id'] )
|| empty( $post_data['item_id'] )
) {
throw new \Exception( __( 'Invalid request.', 'upstream' ) );
}
} else {
$item_id = $project_id;
}
// Check if the user has enough permissions to insert a new comment.
if ( ! upstream_can_access_field( 'publish_project_discussion', $comment_target_item_type, $item_id, UPSTREAM_ITEM_TYPE_PROJECT, $project_id, 'comments', UPSTREAM_PERMISSIONS_ACTION_EDIT, true ) ) {
throw new \Exception( __( "You're not allowed to do this.", 'upstream' ) );
}
// Check if commenting is disabled on the given project.
if ( upstream_are_comments_disabled( $project_id ) ) {
throw new \Exception( __( 'Commenting is disabled on this project.', 'upstream' ) );
}
$user_id = get_current_user_id();
$comment = new Comment( stripslashes( wp_kses_post( $post_data['content'] ) ), $project_id, $user_id );
$comment->parent_id = absint( $post_data['parent_id'] );
$comment->created_by->ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', sanitize_text_field( $server['REMOTE_ADDR'] ) );
$comment->created_by->agent = isset( $server['HTTP_USER_AGENT'] ) ? sanitize_textarea_field( $server['HTTP_USER_AGENT'] ) : null;
$comment->save();
update_comment_meta( $comment->id, 'type', $comment_target_item_type );
if ( 'project' !== $comment_target_item_type ) {
update_comment_meta( $comment->id, 'id', sanitize_text_field( $post_data['item_id'] ) );
}
$use_admin_layout = ! isset( $post_data['teeny'] ) ? true : boolval( $post_data['teeny'] ) === false;
$parent = get_comment( $comment->parent_id );
$comments_cache = array(
$parent->comment_ID => json_decode(
json_encode(
array(
'created_by' => array(
'name' => $parent->comment_author,
),
)
)
),
);
$response['comment_html'] = stripslashes( $comment->render( true, $use_admin_layout, $comments_cache ) );
wp_new_comment_notify_moderator( $comment->id );
wp_notify_postauthor( $comment->id );
$response['success'] = true;
} catch ( \Exception $e ) {
$response['error'] = $e->getMessage();
}
wp_send_json( $response );
}
/**
* AJAX endpoint that trashes a comment.
*
* @throws \Exception Set the error message.
* @since 1.13.0
* @static
*/
public static function trash_comment() {
header( 'Content-Type: application/json' );
$post_data = isset( $_POST ) ? wp_unslash( $_POST ) : array();
$response = array(
'success' => false,
'error' => null,
);
try {
// Check if the request payload is potentially invalid.
if (
! defined( 'DOING_AJAX' )
|| ! DOING_AJAX
|| empty( $post_data )
|| ! isset( $post_data['nonce'] )
|| ! isset( $post_data['project_id'] )
|| ! isset( $post_data['comment_id'] )
|| ! check_ajax_referer( 'upstream:project.trash_comment:' . sanitize_textarea_field( $post_data['comment_id'] ), 'nonce', false )
) {
throw new \Exception( __( 'Invalid request.', 'upstream' ) );
}
// Check if the project exists.
$project_id = absint( $post_data['project_id'] );
if ( $project_id <= 0 ) {
throw new \Exception( __( 'Invalid Project.', 'upstream' ) );
}
// Check if the Discussion/Comments section is disabled for the current project.
if ( upstream_are_comments_disabled( $project_id ) ) {
throw new \Exception( __( 'Comments are disabled for this project.', 'upstream' ) );
}
// Check if the parent comment exists.
$comment_id = absint( $post_data['comment_id'] );
$comment = get_comment( $comment_id );
if ( empty( $comment )
// Check if the comment belongs to that project.
|| (
isset( $comment->comment_post_ID )
&& (int) $comment->comment_post_ID !== $project_id
)
) {
throw new \Exception( _x( 'Comment not found.', 'Removing a comment in projects', 'upstream' ) );
}
$user_id = (int) get_current_user_id();
if ( ! upstream_admin_permissions( 'delete_project_discussion' )
&& ! current_user_can( 'moderate_comments' )
&& (int) $comment->user_id !== $user_id
) {
throw new \Exception( __( "You're not allowed to do this.", 'upstream' ) );
}
$success = wp_trash_comment( $comment );
if ( ! $success ) {
throw new \Exception( __( "It wasn't possible to delete this comment.", 'upstream' ) );
}
$response['success'] = true;
} catch ( \Exception $e ) {
$response['error'] = $e->getMessage();
}
wp_send_json( $response );
}
/**
* AJAX endpoint that unapproves a comment.
*
* @since 1.13.0
* @static
*/
public static function unapprove_comment() {
header( 'Content-Type: application/json' );
$post_data = isset( $_POST ) ? wp_unslash( $_POST ) : array();
$comment_id = isset( $post_data['comment_id'] ) ? absint( $post_data['comment_id'] ) : 0;
check_ajax_referer( 'upstream:project.unapprove_comment:' . $comment_id, 'nonce' );
$response = array(
'success' => false,
'error' => null,
);
try {
$comment = self::toggle_comment_approval_status( $comment_id, false );
$comments = array();
if ( $comment->parent_id > 0 ) {
$parent_comment = get_comment( $comment->parent_id );
if ( is_numeric( $parent_comment->comment_approved ) ) {
if ( (bool) $parent_comment->comment_approved ) {
$comments = array(
$comment->parent_id => json_decode(
json_encode(
array(
'created_by' => array(
'name' => $parent_comment->comment_author,
),
)
)
),
);
} else {
$user = wp_get_current_user();
$user_has_admin_capabilities = upstream_is_user_either_manager_or_admin( $user );
$user_can_moderate_comments = ! $user_has_admin_capabilities ? user_can(
$user,
'moderate_comments'
) : true;
if ( $user_can_moderate_comments ) {
$comments = array(
$comment->parent_id => json_decode(
json_encode(
array(
'created_by' => array(
'name' => $parent_comment->comment_author,
),
)
)
),
);
}
}
}
unset( $parent_comment );
}
$use_admin_layout = ! isset( $post_data['teeny'] ) ? true : boolval( $post_data['teeny'] ) === false;
$response['comment_html'] = $comment->render( true, $use_admin_layout, $comments );
wp_new_comment_notify_moderator( $comment->id );
$response['success'] = true;
} catch ( \Exception $e ) {
$response['error'] = $e->getMessage();
}
wp_send_json( $response );
}
/**
* Either approves/unapproves a given comment.
* This method is called by the correspondent AJAX endpoints.
*
* @since 1.13.0
* @access private
* @static
*
* @throws \Exception When something went wrong or failed on validations.
*
* @param int $comment_id Comment ID being edited.
* @param bool $is_approved Either the comment will be approved or not.
*/
private static function toggle_comment_approval_status( $comment_id, $is_approved ) {
$post_data = isset( $_POST ) ? wp_unslash( $_POST ) : array();
// Check if the request payload is potentially invalid.
if (
! defined( 'DOING_AJAX' )
|| ! DOING_AJAX
|| empty( $post_data )
|| ! isset( $post_data['nonce'] )
|| ! isset( $post_data['project_id'] )
|| ! isset( $post_data['comment_id'] )
|| ! check_ajax_referer(
'upstream:project.' . ( $is_approved ? 'approve_comment' : 'unapprove_comment' ) . ':' . sanitize_textarea_field( $post_data['comment_id'] ),
'nonce',
false
)
) {
throw new \Exception( __( 'Invalid request.', 'upstream' ) );
}
// Check if the user has enough permissions to do this.
if ( ! current_user_can( 'moderate_comments' ) ) {
throw new \Exception( __( "You're not allowed to do this.", 'upstream' ) );
}
// Check if the project potentially exists.
$project_id = absint( $post_data['project_id'] );
if ( $project_id <= 0 ) {
// translators: %s: parameter name.
throw new \Exception( sprintf( __( 'Invalid "%s" parameter.', 'upstream' ), 'project_id' ) );
}
// Check if the Discussion/Comments section is disabled for the current project.
if ( upstream_are_comments_disabled( $project_id ) ) {
throw new \Exception( __( 'Comments are disabled for this project.', 'upstream' ) );
}
$cid = isset( $post_data['comment_id'] ) ? absint( $post_data['comment_id'] ) : 0;
$comment = Comment::load( $cid );
if ( ! ( $comment instanceof Comment ) ) {
throw new \Exception( __( 'Comment not found.', 'upstream' ) );
}
$success = (bool) $is_approved ? $comment->approve() : $comment->unapprove();
if ( ! $success ) {
throw new \Exception( __( 'Unable to save the data into database.', 'upstream' ) );
}
return $comment;
}
/**
* AJAX endpoint that approves a comment.
*
* @since 1.13.0
* @static
*/
public static function approve_comment() {
header( 'Content-Type: application/json' );
$post_data = isset( $_POST ) ? wp_unslash( $_POST ) : array();
$comment_id = isset( $post_data['comment_id'] ) ? absint( $post_data['comment_id'] ) : 0;
check_ajax_referer( 'upstream:project.approve_comment:' . $comment_id, 'nonce' );
$response = array(
'success' => false,
'error' => null,
);
try {
$comment_id = isset( $post_data['comment_id'] ) ? absint( $post_data['comment_id'] ) : 0;
$comment = self::toggle_comment_approval_status( $comment_id, true );
$comments = array();
if ( $comment->parent_id > 0 ) {
$parent_comment = get_comment( $comment->parent_id );
if ( is_numeric( $parent_comment->comment_approved ) ) {
if ( (bool) $parent_comment->comment_approved ) {
$comments = array(
$comment->parent_id => json_decode(
wp_json_encode(
array(
'created_by' => array(
'name' => $parent_comment->comment_author,
),
)
)
),
);
} else {
$user = wp_get_current_user();
$user_has_admin_capabilities = upstream_is_user_either_manager_or_admin( $user );
$user_can_moderate_comments = ! $user_has_admin_capabilities ? user_can(
$user,
'moderate_comments'
) : true;
if ( $user_can_moderate_comments ) {
$comments = array(
$comment->parent_id => json_decode(
json_encode(
array(
'created_by' => array(
'name' => $parent_comment->comment_author,
),
)
)
),
);
}
}
}
unset( $parent_comment );
}
$use_admin_layout = ! isset( $post_data['teeny'] ) ? true : boolval( $post_data['teeny'] ) === false;
$response['comment_html'] = $comment->render( true, $use_admin_layout, $comments );
$response['success'] = true;
} catch ( \Exception $e ) {
$response['error'] = $e->getMessage();
}
wp_send_json( $response );
}
/**
* AJAX endpoint that fetches all comments from a given item/project.
*
* @throws \Exception When something went wrong or failed on validations.
* @since 1.13.0
* @static
*/
public static function fetch_comments() {
header( 'Content-Type: application/json' );
$get_data = isset( $_GET ) ? wp_unslash( $_GET ) : array();
$response = array(
'success' => false,
'data' => array(),
'error' => null,
);
try {
// Check if the request payload is potentially invalid.
if (
! defined( 'DOING_AJAX' )
|| ! DOING_AJAX
|| empty( $get_data )
|| ! isset( $get_data['nonce'] )
|| ! isset( $get_data['project_id'] )
|| ! isset( $get_data['item_type'] )
|| ! self::is_item_type_valid( sanitize_textarea_field( $get_data['item_type'] ) )
) {
throw new \Exception( __( 'Invalid request.', 'upstream' ) );
}
// Check if the project potentially exists.
$project_id = absint( $get_data['project_id'] );
if ( $project_id <= 0 ) {
throw new \Exception( __( 'Invalid Project.', 'upstream' ) );
}
// Prepare data to verify nonce.
$comment_target_item_type = strtolower( sanitize_text_field( $get_data['item_type'] ) );
$item_id = null;
if ( 'project' !== $comment_target_item_type ) {
if (
! isset( $get_data['item_id'] )
|| empty( $get_data['item_id'] )
) {
throw new \Exception( __( 'Invalid request.', 'upstream' ) );
}
// non-numeric id.
$item_id = sanitize_textarea_field( $get_data['item_id'] );
$nonce_identifier = 'upstream:project.' . $comment_target_item_type . 's.fetch_comments';
} else {
$nonce_identifier = 'upstream:project.fetch_comments';
}
// Verify nonce.
if ( ! check_ajax_referer( $nonce_identifier, 'nonce', false ) ) {
throw new \Exception( __( 'Invalid nonce.', 'upstream' ) );
}
// Check if commenting is disabled on the given project.
if ( upstream_are_comments_disabled( $project_id ) ) {
throw new \Exception( __( 'Commenting is disabled on this project.', 'upstream' ) );
}
$use_admin_layout = ! isset( $get_data['teeny'] ) ? true : boolval( $get_data['teeny'] ) === false;
$comments_cache = static::get_comments( $project_id, $comment_target_item_type, $item_id );
foreach ( $comments_cache as $comment ) {
if ( 0 === $comment->parent_id ) {
ob_start();
if ( $use_admin_layout ) {
upstream_admin_display_message_item( $comment, array() );
} else {
upstream_display_message_item( $comment, array() );
}
$response['data'][] = trim( ob_get_contents() );
ob_end_clean();
}
}
$response['success'] = true;
} catch ( \Exception $e ) {
$response['error'] = $e->getMessage();
}
wp_send_json( $response );
}
/**
* Get comments.
*
* @param int $project_id Project ID.
* @param string $item_type Item type.
* @param int $item_id Item ID.
*
* @return array
*/
public static function get_comments( $project_id, $item_type, $item_id = null ) {
$comments_cache = array();
$users_cache = array();
$users_rowset = get_users(
array(
'fields' => array(
'ID',
'display_name',
),
)
);
foreach ( $users_rowset as $user_row ) {
$user_row->ID *= 1;
$users_cache[ $user_row->ID ] = (object) array(
'id' => $user_row->ID,
'name' => $user_row->display_name,
'avatar' => get_userAvatarURL( $user_row->ID ),
);
}
unset( $user_row, $users_rowset );
if ( 'project' === $item_type ) {
$item_id = $project_id;
}
$date_format = get_option( 'date_format' );
$time_format = get_option( 'time_format' );
$the_date_time_format = $date_format . ' ' . $time_format;
$current_timestamp = time();
$user = wp_get_current_user();
$user_has_admin_capabilities = upstream_is_user_either_manager_or_admin( $user );
$user_can_reply = ! $user_has_admin_capabilities ? user_can(
$user,
'publish_project_discussion'
) : true;
$user_can_reply = upstream_override_access_field( $user_can_reply, $item_type, $item_id, UPSTREAM_ITEM_TYPE_PROJECT, $project_id, 'comments', UPSTREAM_PERMISSIONS_ACTION_EDIT );
$user_can_moderate = ! $user_has_admin_capabilities ? user_can( $user, 'moderate_comments' ) : true;
$user_can_delete = ! $user_has_admin_capabilities ? $user_can_moderate || user_can(
$user,
'delete_project_discussion'
) : true;
$user_can_delete = upstream_override_access_field( $user_can_delete, $item_type, $item_id, UPSTREAM_ITEM_TYPE_PROJECT, $project_id, 'comments', UPSTREAM_PERMISSIONS_ACTION_DELETE );
$comments_statuses = array( 'approve' );
if ( $user_has_admin_capabilities || $user_can_moderate ) {
$comments_statuses[] = 'hold';
}
$items_rowset = (array) get_post_meta(
$project_id,
'_upstream_project_' . $item_type . 's',
true
);
if ( count( $items_rowset ) > 0 ) {
foreach ( $items_rowset as $row ) {
if ( empty( $row ) ) {
continue;
}
if ( ! empty( $item_id ) ) {
if ( $item_id != $row['id'] ) {
continue;
}
}
$comments = (array) get_comments(
array(
'post_id' => $project_id,
'status' => $comments_statuses,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'type',
'value' => $item_type,
),
array(
'key' => 'id',
'value' => $row['id'],
),
),
)
);
if ( count( $comments ) > 0 ) {
foreach ( $comments as $comment ) {
$author = $users_cache[ (int) $comment->user_id ];
$date = \DateTime::createFromFormat( 'Y-m-d H:i:s', $comment->comment_date_gmt );
$comment_data = json_decode(
json_encode(
array(
'id' => (int) $comment->comment_ID,
'parent_id' => (int) $comment->comment_parent,
'content' => $comment->comment_content,
'state' => $comment->comment_approved,
'created_by' => $author,
'created_at' => array(
'localized' => '',
'humanized' => sprintf(
// translators: %s: human-readable time difference.
_x( '%s ago', '%s = human-readable time difference', 'upstream' ),
human_time_diff( $date->getTimestamp(), $current_timestamp )
),
),
'current_user_cap' => array(
'can_reply' => $user_can_reply,
'can_moderate' => $user_can_moderate,
'can_delete' => $user_can_delete || $author->id === $user->ID,
),
'replies' => array(),
)
)
);
$comment_data->created_at->localized = $date->format( $the_date_time_format );
$comments_cache[ $comment_data->id ] = $comment_data;
}
foreach ( $comments_cache as $comment ) {
if ( $comment->parent_id > 0 ) {
if ( isset( $comments_cache[ $comment->parent_id ] ) ) {
$comments_cache[ $comment->parent_id ]->replies[] = $comment;
} else {
unset( $comments_cache[ $comment->id ] );
}
}
}
}
}
}
return $comments_cache;
}
/**
* Set additional notification recipients as needed for newly added comments.
*
* @since 1.15.0
* @static
*
* @param array $recipients Recipients list.
* @param int $comment_id The new comment ID.
*
* @return array
*/
public static function define_notification_recipients( $recipients, $comment_id ) {
$should_send = upstream_send_notifications_for_new_comments();
if ( ! $should_send ) {
return array();
}
// 2 minutes.
$transient_expiration = 60 * 2;
$comment = get_comment( $comment_id );
$comment = (object) array(
'id' => (int) $comment->comment_ID,
'project_id' => (int) $comment->comment_post_ID,
'parent' => (int) $comment->comment_parent,
'created_by' => (int) $comment->user_id,
'target' => get_comment_meta( $comment_id, 'type', true ),
'target_id' => (int) $comment->comment_post_ID,
);
// Check if we should disable all emaill notifications for this project.
$meta = (array) get_post_meta( $comment->project_id, '_upstream_project_disable_all_notifications' );
if ( count( $meta ) > 0 && 'on' === $meta[0] ) {
return array();
}
// Check if we need to skip further data processing.
if ( ! in_array( $comment->target, array( 'project', 'milestone', 'task', 'bug', 'file' ) ) ) {
return $recipients;
}
$comment->target_label = call_user_func( 'upstream_' . $comment->target . '_label' );
if ( 'project' !== $comment->target ) {
$comment->target_id = get_comment_meta( $comment_id, 'id', true );
}
set_transient( 'upstream:comment_notification.comment:' . $comment_id, $comment, $transient_expiration );
$get_user = function ( $user_id ) use ( $transient_expiration ) {
if ( $user_id <= 0 ) {
return null;
}
// Check if the user is cached.
$user = get_transient( 'upstream:comment_notification.user:' . $user_id );
if ( empty( $user ) ) {
// Check if the user exists.
$user = get_user_by( 'id', $user_id );
if ( false === $user ) {
return null;
}
// Prepare user data.
$user = (object) array(
'id' => (int) $user->ID,
'name' => (string) $user->display_name,
'email' => (string) $user->user_email,
);
// Cache user.
set_transient( 'upstream:comment_notification.user:' . $user->id, $user, $transient_expiration );
}
return $user;
};
$fetch_project_meta_as_map = function ( $project_id, $key, &$map ) use ( $transient_expiration, $get_user ) {
$rowset = array();
if ( 'milestone' === $key ) {
$rowset = (array) ( \UpStream\Milestones::getInstance()->get_milestones_as_rowset( $project_id ) );
} else {
$rowset = (array) get_post_meta( $project_id, '_upstream_project_' . $key . 's', true );
}
foreach ( $rowset as $row ) {
$title_key = 'milestone' !== $key ? 'title' : 'milestone';
if ( isset( $row['id'] )
&& ! empty( $row['id'] )
&& isset( $row[ $title_key ] )
&& ! empty( $row[ $title_key ] )
) {
$item = (object) array(
'id' => $row['id'],
'title' => $row[ $title_key ],
'assigned_to' => isset( $row['assigned_to'] ) ? $row['assigned_to'] : array(),
'created_by' => isset( $row['created_by'] ) ? (int) $row['created_by'] : 0,
'type' => $key,
);
if ( count( $item->assigned_to ) > 0 ) {
foreach ( $item->assigned_to as $a ) {
$user = $get_user( $a );
$recipients[] = $user->email;
}
}
if ( $item->created_by > 0 ) {
$user = $get_user( $item->created_by );
if ( empty( $user ) ) {
$item->created_by = 0;
} else {
$item->created_by = $user->id;
}
}
$map[ $item->id ] = $item;
}
}
};
// RSD: this cache is causing issues
// $project = get_transient('upstream:comment_notification.project:' . $comment->project_id).
if ( empty( $project ) ) {
$project = get_post( $comment->project_id );
$project = (object) array(
'id' => (int) $project->ID,
'title' => $project->post_title,
'created_by' => (int) $project->post_author,
'owner_id' => (int) get_post_meta( $project->ID, '_upstream_project_owner', true ),
'owner_email' => '',
'milestones' => array(),
'tasks' => array(),
'bugs' => array(),
'files' => array(),
);
if ( $project->owner_id > 0 ) {
$owner = get_transient( 'upstream:comment_notification.user:' . $project->owner_id );
if ( empty( $owner ) ) {
$owner = get_user_by( 'id', $project->owner_id );
$owner = (object) array(
'id' => $project->owner_id,
'name' => (string) $owner->display_name,
'email' => (string) $owner->user_email,
);
set_transient( 'upstream:comment_notification.user:' . $owner->id, $owner, $transient_expiration );
}
$pms = upstream_project_members_ids( $comment->project_id );
foreach ( $pms as $pm ) {
$user_info = get_userdata( $pm );
$email = $user_info->user_email;
$recipients[] = $email;
}
}
if ( 'project' !== $comment->target ) {
$fetch_project_meta_as_map( $project->id, $comment->target, $project->{$comment->target . 's'} );
foreach ( $project->{$comment->target . 's'} as $item ) {
$r = $comment->target_id;
if ( $item->id === $comment->target_id ) {
if ( count( $item->assigned_to ) > 0 ) {
foreach ( $item->assigned_to as $a ) {
$user = $get_user( $a );
$recipients[] = $user->email;
}
}
if ( $item->created_by > 0 ) {
$user = $get_user( $item->created_by );
$recipients[] = $user->email;
}
}
}
}
set_transient(
'upstream:comment_notification.project:' . $comment->project_id,
$project,
$transient_expiration
);
} else {
if ( 'project' !== $comment->target
&& empty( $project->{$comment->target . 's'} )
) {
$fetch_project_meta_as_map( $project->id, $comment->target, $project->{$comment->target . 's'} );
set_transient(
'upstream:comment_notification.project:' . $comment->project_id,
$project,
$transient_expiration
);
}
}
if ( $comment->parent > 0 ) {
$parent_id = $comment->parent;
$users_cache = array();
do {
$parent_comment = get_comment( $parent_id );
$parent_exists = ! empty( $parent_comment );
if ( $parent_exists ) {
if ( ! isset( $users_cache[ $parent_comment->user_id ] ) ) {
$users_cache[ $parent_comment->user_id ] = $get_user( $parent_comment->user_id );
$users_cache[ $parent_comment->user_id ]->notify = upstream_user_can_receive_comment_replies_notification( $parent_comment->user_id );
}
$user = &$users_cache[ $parent_comment->user_id ];
$parent_comment_author = $get_user( $parent_comment->user_id );
if ( $user->notify ) {
$recipients[] = $parent_comment_author->email;
}
$parent_id = (int) $parent_comment->comment_parent;
}
} while ( $parent_exists );
}
$recipients = array_unique( array_filter( $recipients ) );
$recipients = apply_filters( 'upstream:comment_notification.recipients', $recipients, $comment );
return $recipients;
}
/**
* Add additional info to comment notifications subject.
*
* @since 1.15.0
* @static
*
* @param string $subject The original subject.
* @param int $comment_id The new comment ID.
*
* @return string
*/
public static function define_notification_header( $subject, $comment_id ) {
$comment = get_transient( 'upstream:comment_notification.comment:' . $comment_id );
// Check if we need to skip further data processing in case of comments written outside UpStream's scope.
if ( empty( $comment )
|| in_array( $comment->target, array( 'project', 'milestone', 'task', 'bug', 'file' ) )
) {
return $subject;
}
$project = get_transient( 'upstream:comment_notification.project:' . $comment->project_id );
$site_name = get_bloginfo( 'name' );
$subject = sprintf(
'[%s][%s] %s',
$site_name,
$project->title,
sprintf(
// translators: %s: Comment notification subject.
_x( 'New comment on %s', 'Comment notification subject', 'upstream' ),
$comment->target_label
)
);
if ( 'project' !== $comment->target ) {
$meta = (array) get_post_meta( $project->id, '_upstream_project_' . $comment->target . 's', true );
foreach ( $meta as $item ) {
if ( isset( $item['id'] ) && $item['id'] === $comment->target_id ) {
$title_key = 'milestone' === $comment->target ? 'milestone' : 'title';
if ( isset( $item[ $title_key ] ) ) {
$subject .= sprintf( ': "%s"', $item[ $title_key ] );
}
break;
}
}
}
$subject = apply_filters( 'upstream:comment_notification.subject', $subject, $comment, $project );
return $subject;
}
/**
* Add items title to notification.
*
* @param string $comment_text Comment text.
* @param int $comment_id Comment ID.
*
* @return mixed
*/
public static function add_item_title_to_notification( $comment_text, $comment_id ) {
// Check if the comment has item_title in the metadata.
$item_title = get_comment_meta( $comment_id, 'title', true );
$item_type = get_comment_meta( $comment_id, 'type', true );
if ( ! empty( $item_title ) ) {
if ( 'milestone' === $item_type ) {
// Get the milestone's title.
$milestones = upstream_get_milestones_titles();
if ( isset( $milestones[ $item_title ] ) ) {
$item_title = $milestones[ $item_title ];
}
}
$comment_text = __( 'Item Title: ', 'upstream' ) . $item_title . "\r\n\r\n" . $comment_text;
}
if ( ! empty( $item_type ) ) {
$labels = upstream_get_default_labels();
$item_type_label = $labels[ $item_type . 's' ]['singular'];
$comment_text = __( 'Item Type: ', 'upstream' ) . $item_type_label . "\r\n" . $comment_text;
}
return $comment_text;
}
/**
* Convert notifications text for comments in projects into HTML.
*
* @param string $text Comment text.
* @param int $comment_id Comment ID.
*
* @return string
*/
public static function filter_comment_notification_text( $text, $comment_id ) {
if ( self::is_comment_from_project( $comment_id ) ) {
// Convert from txt to html.
$text = str_replace( "\n", '
', $text );
$text = self::replace_email_with_html_link( $text );
$text = self::replace_link_with_html_link( $text );
}
return $text;
}
/**
* Is comment from project.
*
* @param int $comment_id Comment ID.
*
* @return bool
*/
protected static function is_comment_from_project( $comment_id ) {
// Check if the post is a project.
$comment = get_comment( $comment_id );
$post = get_post( $comment->comment_post_ID );
return 'project' === $post->post_type;
}
/**
* Replace email with HTML link.
*
* @param string $text Email text.
*
* @return string
*/
protected static function replace_email_with_html_link( $text ) {
$text = preg_replace( '/([^\s@]+@[a-z\._\-0-9]+)/i', '${1}', $text );
return $text;
}
/**
* Replace link with HTML link.
*
* @param string $text Link.
*
* @return string
*/
protected static function replace_link_with_html_link( $text ) {
$text = preg_replace( '~([a-z]+:\/\/\S+)~i', '${1}', $text );
return $text;
}
/**
* Convert notifications for comments in projects into HTML.
*
* @param string $headers Headers.
* @param int $comment_id Comment ID.
*
* @return string
*/
public static function filter_comment_notification_headers( $headers, $comment_id ) {
if ( self::is_comment_from_project( $comment_id ) ) {
// Convert from txt to html.
$headers = str_replace( 'text/plain;', 'text/html;', $headers );
}
return $headers;
}
}