config = $config; } /** * The workflow governing a post, or null. * * @param \WP_Post $post Post. * @return array|null */ public function for_post( $post ) { if ( ! $post instanceof \WP_Post ) { return null; } return $this->config->for_post_type( $post->post_type ); } /** * Find the step definition for a status within a workflow. * * @param array $workflow Workflow definition. * @param string $status Post status. * @return array|null */ public function step( array $workflow, $status ) { foreach ( $workflow['steps'] as $step ) { if ( isset( $step['status'] ) && $step['status'] === $status ) { return $step; } } return null; } /** * Where a "submit" from the current (draft) step should route: the conditional * target if the post carries a matching flag term, else the default target. * * @param array $step Step definition. * @param \WP_Post $post Post. * @return string */ public function submit_target( array $step, $post ) { if ( isset( $step['conditional_to']['condition'], $step['conditional_to']['status'] ) ) { $cond = $step['conditional_to']['condition']; if ( ! empty( $cond['taxonomy'] ) && ! empty( $cond['terms'] ) && has_term( $cond['terms'], $cond['taxonomy'], $post ) ) { return $step['conditional_to']['status']; } } return isset( $step['advance_to'] ) ? $step['advance_to'] : 'pending'; } /** * The actions a given user may take on a given post right now. * * Each action: [ 'action' => submit|approve|reject, 'label', 'target', 'needs_note' ]. * * @param \WP_Post $post Post. * @param \WP_User|int|null $user User (defaults to current). * @return array */ public function allowed_actions( $post, $user = null ) { $workflow = $this->for_post( $post ); if ( ! $workflow ) { return array(); } $user = $this->resolve_user( $user ); $step = $this->step( $workflow, $post->post_status ); // New/unsaved posts (auto-draft, new, pending) enter at the actor step so the // author's "Submit for Review" is available before the first manual save. if ( ! $step && $this->is_entry_status( $post->post_status ) ) { $step = $this->entry_step( $workflow ); } if ( ! $step ) { return array(); } $is_admin = $this->is_global_approver( $user ); $actions = array(); // Actor step (draft): submit for review. if ( isset( $step['advance_to'] ) || isset( $step['conditional_to'] ) ) { if ( $is_admin || $this->has_any_role( $user, isset( $step['actor_roles'] ) ? $step['actor_roles'] : array() ) ) { $actions[] = array( 'action' => 'submit', 'label' => isset( $step['advance_label'] ) ? $step['advance_label'] : __( 'Submit for Review', 'ost-editorial-workflow' ), 'target' => $this->submit_target( $step, $post ), 'needs_note' => false, ); } } // Approver step: approve / send back. if ( isset( $step['approver_roles'] ) ) { if ( $is_admin || $this->has_any_role( $user, $step['approver_roles'] ) ) { if ( isset( $step['on_approve'] ) ) { $actions[] = array( 'action' => 'approve', 'label' => __( 'Approve', 'ost-editorial-workflow' ), 'target' => $step['on_approve'], 'needs_note' => false, ); } if ( isset( $step['on_reject'] ) ) { $actions[] = array( 'action' => 'reject', 'label' => __( 'Send back', 'ost-editorial-workflow' ), 'target' => $step['on_reject'], 'needs_note' => true, ); } } } /** * Filter the actions available to a user on a post (section-scoping hooks here too). * * @param array $actions Allowed actions. * @param \WP_Post $post Post. * @param \WP_User $user User. */ return apply_filters( 'ostew/allowed_actions', $actions, $post, $user ); } /** * Find a specific allowed action for a user, or null. * * @param \WP_Post $post Post. * @param string $action Action key. * @param \WP_User|int|null $user User. * @return array|null */ public function find_action( $post, $action, $user = null ) { foreach ( $this->allowed_actions( $post, $user ) as $a ) { if ( $a['action'] === $action ) { return $a; } } return null; } /** * Does this user act as an approver on every step, regardless of the roles a * step lists? * * This is the single definition of "can approve anything". The action handler, * the review queue and the notification resolver all read it, so they cannot * disagree about who an approver is. * * @param \WP_User|int|null $user User (defaults to current). * @return bool */ public function is_global_approver( $user = null ) { $user = $this->resolve_user( $user ); $value = ( $user instanceof \WP_User ) && $user->ID && user_can( $user, 'manage_options' ); /** * Filter whether a user approves on every step. * * Return false to make administrators ordinary users in the workflow, so * that only the roles named in each step may approve. * * @param bool $value Whether the user is a global approver. * @param \WP_User|int|null $user The user being tested. */ return (bool) apply_filters( 'ostew/is_global_approver', $value, $user ); } /** * The users who approve on every step. * * Used when resolving notification recipients, where a capability test cannot * be applied without loading every user on the site. * * @return \WP_User[] */ public function global_approver_users() { $users = get_users( array( 'role__in' => array( 'administrator' ) ) ); /** * Filter the users who approve on every step. * * @param \WP_User[] $users Users treated as approvers everywhere. */ $users = apply_filters( 'ostew/global_approvers', $users ); return is_array( $users ) ? $users : array(); } /** * Statuses that count as "before the workflow starts" (author entry point). * * @return string[] */ public function entry_statuses() { return array( 'auto-draft', 'new', 'draft', 'pending', '' ); } /** * Is a status a pre-workflow entry status? * * @param string $status Status. * @return bool */ public function is_entry_status( $status ) { return in_array( $status, $this->entry_statuses(), true ); } /** * The actor/entry step of a workflow (the first step that can advance forward). * * @param array $workflow Workflow. * @return array|null */ public function entry_step( array $workflow ) { foreach ( $workflow['steps'] as $step ) { if ( isset( $step['advance_to'] ) || isset( $step['conditional_to'] ) ) { return $step; } } return null; } /** * Human label for a status (step label if defined, else humanized key). * * @param array $workflow Workflow. * @param string $status Status key. * @return string */ public function status_label( array $workflow, $status ) { $step = $this->step( $workflow, $status ); if ( $step && isset( $step['label'] ) ) { return $step['label']; } // Present unsaved states as "Draft" rather than the raw "auto-draft". if ( in_array( $status, array( 'auto-draft', 'new', '' ), true ) ) { $obj = get_post_status_object( 'draft' ); return $obj ? $obj->label : __( 'Draft', 'ost-editorial-workflow' ); } $obj = get_post_status_object( $status ); if ( $obj && isset( $obj->label ) ) { return $obj->label; } return ucwords( str_replace( array( 'ostew_', '_' ), array( '', ' ' ), $status ) ); } /** * Does the user hold any of the given roles? * * @param \WP_User $user User. * @param array $roles Role slugs. * @return bool */ private function has_any_role( $user, array $roles ) { if ( ! $user instanceof \WP_User || empty( $roles ) ) { return false; } return (bool) array_intersect( $roles, (array) $user->roles ); } /** * Normalise a user argument to a WP_User. * * @param \WP_User|int|null $user User. * @return \WP_User */ private function resolve_user( $user ) { if ( $user instanceof \WP_User ) { return $user; } if ( is_numeric( $user ) ) { return get_user_by( 'id', (int) $user ); } return wp_get_current_user(); } }