$texts, 'hold' => '', 'note' => '', ); if ( SpaceAccess::can_moderate( $space, $user_id ) ) { return $result; } $found = array(); foreach ( $texts as $text ) { $found = array_merge( $found, WordFilter::find( $text ) ); } $found = array_values( array_unique( $found ) ); $is_edit = in_array( $kind, array( 'post_edit', 'comment_edit' ), true ); if ( $found ) { $action = Settings::get( 'blocked_words_action', 'review' ); if ( 'mask' === $action ) { $result['texts'] = array_map( array( WordFilter::class, 'mask' ), $texts ); } elseif ( 'block' === $action || $is_edit ) { return new WP_Error( 'spacebay_blocked_words', __( 'Your text contains words that are not allowed in this community. Please change it and try again.', 'spacebay' ), array( 'status' => 400 ) ); } else { $result['hold'] = 'words'; $result['note'] = implode( ', ', $found ); } } if ( '' === $result['hold'] && 'post_edit' === $kind && 'all' === Settings::get( 'post_review', 'off' ) ) { $result['hold'] = 'edited'; } if ( '' === $result['hold'] && 'post' === $kind ) { $review = Settings::get( 'post_review', 'off' ); if ( 'all' === $review ) { $result['hold'] = 'all_posts'; } elseif ( 'first' === $review && ! self::has_published_post( $user_id ) ) { $result['hold'] = 'first_post'; } } /** * Filters whether content is held for review. * * @since 1.0.0 * * @param string $hold Reason key, or "" to publish. * @param string $kind Content kind. * @param int $user_id Author. * @param array $texts Texts after masking. */ $result['hold'] = sanitize_key( (string) apply_filters( 'spacebay_hold_content', $result['hold'], $kind, $user_id, $result['texts'] ) ); return $result; } /** * Called after content is saved as pending. * * @param string $type "post" or "comment". * @param array $row Content row. * @param string $reason Hold reason. * @param string $note Detail. * @return void */ public static function held( $type, array $row, $reason, $note = '' ) { Reports::hold( $type, $row['id'], $reason, $note ); /** * Fires when content is held for review. * * @since 1.0.0 * * @param string $type "post" or "comment". * @param array $row Content row. * @param string $reason Reason key. */ do_action( 'spacebay_content_held', $type, $row, $reason ); } /** * Hides reported content once enough members reported it. * * @param string $type "post" or "comment". * @param int $id Object ID. * @return bool Whether the content was hidden. */ public static function after_report( $type, $id ) { $threshold = (int) Settings::get( 'report_threshold', 3 ); if ( $threshold < 1 || ! in_array( $type, array( 'post', 'comment' ), true ) || Reports::open_member_reports( $type, $id ) < $threshold ) { return false; } $found = self::content( $type, $id ); if ( ! $found || 'published' !== $found['row']['status'] || SpaceAccess::can_moderate( $found['space'], $found['row']['user_id'] ) ) { return false; } $row = 'post' === $type ? Posts::set_status( $found['row'], 'pending' ) : Comments::set_status( $found['row'], 'pending' ); self::held( $type, $row, 'reports' ); return true; } /** * Keeps reported or held content: publishes it and closes its reports. * * @param string $type "post", "comment" or "member". * @param int $id Object ID. * @param int $moderator_id Moderator. * @return true|WP_Error */ public static function approve( $type, $id, $moderator_id ) { if ( 'member' === $type || 'message' === $type ) { Reports::resolve( $type, $id, 'dismissed', $moderator_id ); return true; } $found = self::content( $type, $id ); if ( ! $found ) { Reports::resolve( $type, $id, 'removed', $moderator_id ); return new WP_Error( 'spacebay_content_gone', __( 'This content no longer exists.', 'spacebay' ), array( 'status' => 404 ) ); } $row = $found['row']; $reasons = wp_list_pluck( Reports::open_for( $type, $id ), 'reason' ); $is_new = (bool) array_intersect( $reasons, array( 'words', 'first_post', 'all_posts' ) ); Reports::resolve( $type, $id, $reasons && 'pending' !== $row['status'] ? 'dismissed' : 'approved', $moderator_id ); if ( 'pending' !== $row['status'] ) { return true; } if ( 'post' === $type ) { $row = Posts::set_status( $row, 'published', $is_new ); if ( $is_new ) { /** This action is documented in includes/Feed/Posts.php */ do_action( 'spacebay_post_created', $row, (array) ( $row['meta']['mentions'] ?? array() ) ); } } else { $row = Comments::set_status( $row, 'published' ); if ( $is_new ) { $post = Posts::find( $row['post_id'] ); /** This action is documented in includes/Feed/Comments.php */ do_action( 'spacebay_comment_created', $row, $post, \SpaceBay\Content\Renderer::render( $row['message'] )['mentions'] ); } } /** * Fires when a moderator approves held content. * * @since 1.0.0 * * @param string $type "post" or "comment". * @param array $row Content row, now published. * @param int $moderator_id Moderator. * @param bool $is_new True for new content held before it was * published; false for content restored * after it was hidden by reports. */ do_action( 'spacebay_content_approved', $type, $row, $moderator_id, $is_new ); return true; } /** * Deletes reported or held content, or suspends a reported member. * * @param string $type "post", "comment" or "member". * @param int $id Object ID. * @param int $moderator_id Moderator. * @return true|WP_Error */ public static function remove( $type, $id, $moderator_id ) { if ( 'member' === $type ) { if ( SpaceAccess::is_manager( $id ) || user_can( $id, Capabilities::MODERATE ) ) { return new WP_Error( 'spacebay_cannot_block', __( 'Community managers and moderators cannot be suspended.', 'spacebay' ), array( 'status' => 400 ) ); } Reports::resolve( 'member', $id, 'removed', $moderator_id ); self::set_member_status( $id, 'blocked', $moderator_id ); return true; } if ( 'message' === $type ) { Reports::resolve( 'message', $id, 'removed', $moderator_id ); $message = \SpaceBay\Messages\Messages::find( $id ); if ( $message && 'text' === $message['type'] ) { \SpaceBay\Messages\Messages::delete( $message ); } return true; } $found = self::content( $type, $id ); Reports::resolve( $type, $id, 'removed', $moderator_id ); if ( ! $found ) { return true; } /** * Fires before a moderator removes content. * * @since 1.0.0 * * @param string $type "post" or "comment". * @param array $row Content row. * @param array $space Space row. * @param int $moderator_id Moderator. */ do_action( 'spacebay_content_removed', $type, $found['row'], $found['space'], $moderator_id ); if ( 'post' === $type ) { Posts::delete( array( $id ) ); } else { Comments::delete( $found['row'] ); } return true; } /** * Approves, blocks or unblocks a member. * * @param int $user_id Member. * @param string $status "active" or "blocked". * @param int $moderator_id Moderator. * @return true|WP_Error */ public static function set_member_status( $user_id, $status, $moderator_id ) { if ( ! get_userdata( $user_id ) ) { return new WP_Error( 'spacebay_member_not_found', __( 'Member not found.', 'spacebay' ), array( 'status' => 404 ) ); } if ( 'blocked' === $status && ( SpaceAccess::is_manager( $user_id ) || user_can( $user_id, Capabilities::MODERATE ) ) ) { return new WP_Error( 'spacebay_cannot_block', __( 'Community managers and moderators cannot be suspended.', 'spacebay' ), array( 'status' => 400 ) ); } $old = Profiles::set_status( $user_id, $status ); if ( false !== $old ) { delete_transient( 'spacebay_admin_stats' ); /** * Fires after a member's account status changes. * * @since 1.0.0 * * @param int $user_id Member. * @param string $status New status: active, pending or blocked. * @param string $old Previous status. * @param int $moderator_id Moderator. */ do_action( 'spacebay_member_status_changed', (int) $user_id, $status, $old, (int) $moderator_id ); } return true; } /** * IDs of community moderators and managers who get review notices. * * @return int[] */ public static function moderator_ids() { $ids = get_users( array( 'capability__in' => array( Capabilities::MANAGE, Capabilities::MODERATE ), 'fields' => 'ID', 'number' => 50, ) ); return array_map( 'intval', (array) $ids ); } /** * Closes reports of a deleted post. * * @param array $post Deleted post row. * @return void */ public static function on_post_deleted( $post ) { Reports::resolve( 'post', (int) $post['id'], 'removed', get_current_user_id() ); } /** * Closes reports of a deleted comment. * * @param array $comment Deleted comment row. * @return void */ public static function on_comment_deleted( $comment ) { Reports::resolve( 'comment', (int) $comment['id'], 'removed', get_current_user_id() ); } /** * Finds a post or comment with its space. * * @param string $type "post" or "comment". * @param int $id Object ID. * @return array{row: array, space: array, post: array}|null */ public static function content( $type, $id ) { $row = 'comment' === $type ? Comments::find( $id ) : Posts::find( $id ); $post = null; if ( $row ) { $post = 'comment' === $type ? Posts::find( $row['post_id'] ) : $row; } $space = $post ? SpaceRepository::find( $post['space_id'] ) : null; if ( ! $row || ! $space ) { return null; } return array( 'row' => $row, 'space' => $space, 'post' => $post, ); } /** * Whether the member has at least one published post. * * @param int $user_id User ID. * @return bool */ private static function has_published_post( $user_id ) { global $wpdb; return (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Fresh check while posting. $wpdb->prepare( 'SELECT id FROM %i WHERE user_id = %d AND status = %s LIMIT 1', Tables::name( 'posts' ), absint( $user_id ), 'published' ) ); } }