defaults() : $stored; /** * Filter the full set of workflow definitions. * * @param array $data Workflow definitions keyed by workflow id. */ $data = apply_filters( 'ostew/workflows', $data ); return is_array( $data ) ? $data : array(); } /** * Get a single workflow by id. * * @param string $id Workflow id. * @return array|null */ public function get( $id ) { $all = $this->all(); return isset( $all[ $id ] ) ? $all[ $id ] : null; } /** * Resolve the workflow that governs a given post type (first match wins). * * @param string $post_type Post type slug. * @return array|null */ public function for_post_type( $post_type ) { foreach ( $this->all() as $workflow ) { if ( ! empty( $workflow['post_types'] ) && in_array( $post_type, (array) $workflow['post_types'], true ) ) { return $workflow; } } return null; } /** * The set of post types under any workflow. * * @return string[] */ public function managed_post_types() { $types = array(); foreach ( $this->all() as $workflow ) { if ( ! empty( $workflow['post_types'] ) ) { $types = array_merge( $types, (array) $workflow['post_types'] ); } } return array_values( array_unique( $types ) ); } /** * Seed the option from defaults if it has never been set. * * @return void */ public function seed() { if ( null === get_option( self::OPTION, null ) ) { add_option( self::OPTION, $this->defaults() ); } } /** * Persist workflows. * * The statuses in the incoming set are remembered first, so that a later * edit which drops one of them can still be reconciled against content. * * @param array $workflows Workflow definitions. * @return bool */ public function update( array $workflows ) { $this->remember_statuses( $this->custom_statuses_in( $workflows ) ); return update_option( self::OPTION, $workflows ); } /** * Every custom (non-core) status defined by a given set of workflows. * * @param array $workflows Workflow definitions. * @return string[] */ public function custom_statuses_in( $workflows ) { $out = array(); foreach ( (array) $workflows as $workflow ) { if ( empty( $workflow['steps'] ) || ! is_array( $workflow['steps'] ) ) { continue; } foreach ( $workflow['steps'] as $step ) { if ( empty( $step['status'] ) || in_array( $step['status'], Statuses::CORE, true ) ) { continue; } $out[] = $step['status']; } } return array_values( array_unique( $out ) ); } /** * Statuses this install has ever had configured. * * @return string[] */ public function known_statuses() { $known = get_option( self::KNOWN_OPTION, array() ); return is_array( $known ) ? array_values( array_unique( array_filter( $known ) ) ) : array(); } /** * Add statuses to the known set. Writes only when something is new. * * @param string[] $statuses Status keys. * @return void */ public function remember_statuses( array $statuses ) { $known = $this->known_statuses(); $merged = array_values( array_unique( array_merge( $known, array_filter( $statuses ) ) ) ); sort( $merged ); sort( $known ); if ( $merged !== $known ) { update_option( self::KNOWN_OPTION, $merged, false ); } } /** * Drop statuses from the known set (used once no content holds them). * * @param string[] $statuses Status keys. * @return void */ public function forget_statuses( array $statuses ) { if ( empty( $statuses ) ) { return; } $known = $this->known_statuses(); $remaining = array_values( array_diff( $known, $statuses ) ); if ( $remaining !== $known ) { update_option( self::KNOWN_OPTION, $remaining, false ); } } /** * Statuses that exist in the current configuration but not in a proposed one. * * @param array $workflows Proposed workflow definitions. * @return string[] */ public function statuses_removed_by( $workflows ) { return array_values( array_diff( $this->custom_statuses_in( $this->all() ), $this->custom_statuses_in( $workflows ) ) ); } /** * Reset to the shipped default (removes the stored override). * * @return void */ public function reset() { delete_option( self::OPTION ); } /** * Validate a set of workflow definitions. Returns a list of human-readable errors * (empty = valid). * * @param mixed $workflows Decoded workflows. * @return string[] */ public function validate( $workflows ) { $errors = array(); $core = array( 'draft', 'pending', 'publish', 'private', 'future' ); if ( ! is_array( $workflows ) || empty( $workflows ) ) { return array( __( 'Configuration must be a non-empty object of workflows.', 'ost-editorial-workflow' ) ); } foreach ( $workflows as $id => $wf ) { if ( empty( $wf['post_types'] ) || ! is_array( $wf['post_types'] ) ) { $errors[] = sprintf( /* translators: %s: workflow id. */ __( 'Workflow "%s" lists no post types, so it would manage nothing.', 'ost-editorial-workflow' ), $id ); } else { foreach ( $wf['post_types'] as $post_type ) { if ( ! is_string( $post_type ) || ! post_type_exists( $post_type ) ) { $errors[] = sprintf( /* translators: 1: post type slug, 2: workflow id. */ __( 'Unknown post type "%1$s" in workflow "%2$s".', 'ost-editorial-workflow' ), is_string( $post_type ) ? $post_type : gettype( $post_type ), $id ); } } } if ( empty( $wf['steps'] ) || ! is_array( $wf['steps'] ) ) { $errors[] = sprintf( /* translators: %s: workflow id. */ __( 'Workflow "%s" has no steps.', 'ost-editorial-workflow' ), $id ); continue; } $statuses = array(); $targets = array(); foreach ( $wf['steps'] as $step ) { if ( empty( $step['status'] ) ) { $errors[] = sprintf( /* translators: %s: workflow id. */ __( 'Workflow "%s" has a step with no status.', 'ost-editorial-workflow' ), $id ); continue; } $statuses[] = $step['status']; foreach ( array( 'advance_to', 'on_approve', 'on_reject' ) as $k ) { if ( isset( $step[ $k ] ) ) { $targets[] = $step[ $k ]; } } if ( isset( $step['conditional_to']['status'] ) ) { $targets[] = $step['conditional_to']['status']; } foreach ( array( 'actor_roles', 'approver_roles' ) as $rk ) { if ( ! empty( $step[ $rk ] ) ) { foreach ( (array) $step[ $rk ] as $role ) { if ( 'administrator' !== $role && ! get_role( $role ) ) { $errors[] = sprintf( /* translators: 1: role, 2: workflow id. */ __( 'Unknown role "%1$s" in workflow "%2$s".', 'ost-editorial-workflow' ), $role, $id ); } } } } } foreach ( $targets as $t ) { if ( ! in_array( $t, $statuses, true ) && ! in_array( $t, $core, true ) ) { $errors[] = sprintf( /* translators: 1: status, 2: workflow id. */ __( 'Target status "%1$s" in workflow "%2$s" is not defined.', 'ost-editorial-workflow' ), $t, $id ); } } if ( ! in_array( 'publish', $targets, true ) && ! in_array( 'publish', $statuses, true ) ) { $errors[] = sprintf( /* translators: %s: workflow id. */ __( 'Workflow "%s" cannot reach "publish".', 'ost-editorial-workflow' ), $id ); } } return $errors; } }