workflow = $workflow; $this->audit = $audit; $this->config = $config; } /** * Register hooks. * * @return void */ public function register_hooks() { add_action( 'transition_post_status', array( $this, 'on_transition' ), 10, 3 ); add_filter( 'wp_insert_post_data', array( $this, 'preserve_author' ), 10, 2 ); } /** * Integrity guard: a workflow save must never change a managed post's author. * A reviewer editing/advancing someone else's post should not become its author. * Administrators may still reassign deliberately. * * @param array $data Sanitized post data about to be saved. * @param array $postarr Raw post array (has ID for updates). * @return array */ public function preserve_author( $data, $postarr ) { if ( empty( $postarr['ID'] ) ) { return $data; // new post — let author be set normally. } if ( empty( $data['post_type'] ) || ! $this->config->for_post_type( $data['post_type'] ) ) { return $data; } $existing = get_post( (int) $postarr['ID'] ); if ( ! $existing ) { return $data; } if ( (int) $data['post_author'] === (int) $existing->post_author ) { return $data; } // Author is being changed. Allow it only for the original author or an admin. if ( (int) get_current_user_id() !== (int) $existing->post_author && ! current_user_can( 'manage_options' ) ) { $data['post_author'] = $existing->post_author; } return $data; } /** * Perform a workflow action requested by a user. * * @param int $post_id Post ID. * @param string $action Action key (submit|approve|reject). * @param string $note Optional editorial note. * @param \WP_User|int|null $user Acting user (defaults to current). * @return string|\WP_Error New status on success, WP_Error otherwise. */ public function handle( $post_id, $action, $note = '', $user = null ) { $post = get_post( $post_id ); if ( ! $post ) { return new \WP_Error( 'ostew_no_post', __( 'Item not found.', 'ost-editorial-workflow' ) ); } $match = $this->workflow->find_action( $post, $action, $user ); if ( ! $match ) { return new \WP_Error( 'ostew_not_allowed', __( 'You are not allowed to perform that action on this item.', 'ost-editorial-workflow' ), array( 'status' => 403 ) ); } $this->pending_note = ( 'reject' === $action ) ? sanitize_textarea_field( $note ) : ''; $this->pending_action = (string) $action; $result = wp_update_post( array( 'ID' => $post->ID, 'post_status' => $match['target'], ), true ); if ( is_wp_error( $result ) ) { $this->pending_note = ''; $this->pending_action = ''; return $result; } $this->pending_action = ''; return $match['target']; } /** * Audit + stamp on every managed status change (choke point). * * @param string $new_status New status. * @param string $old_status Old status. * @param \WP_Post $post Post. * @return void */ public function on_transition( $new_status, $old_status, $post ) { if ( $new_status === $old_status ) { return; } if ( wp_is_post_revision( $post ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) ) { return; } if ( in_array( $new_status, array( 'auto-draft', 'inherit' ), true ) || 'auto-draft' === $old_status ) { return; } if ( ! $this->config->for_post_type( $post->post_type ) ) { return; } $note = $this->pending_note; $action = $this->pending_action; $user_id = get_current_user_id(); // Stamp time-in-status + reset escalation counters (the escalation scanner reads these). update_post_meta( $post->ID, '_ostew_status_since', current_time( 'mysql' ) ); update_post_meta( $post->ID, '_ostew_escalation_count', 0 ); delete_post_meta( $post->ID, '_ostew_last_escalated' ); $this->audit->write( $post->ID, $old_status, $new_status, $user_id, $note, $action ); /** * Fires after a managed workflow transition. notifications hook here. * * @param int $post_id Post ID. * @param string $old_status Old status. * @param string $new_status New status. * @param int $user_id Acting user ID. * @param string $note Editorial note (send-back). */ do_action( 'ostew/after_transition', $post->ID, $old_status, $new_status, $user_id, $note ); $this->pending_note = ''; $this->pending_action = ''; } }