<?php
/**
* Automation REST controller.
*
* @package automattic/jetpack-crm
*/
namespace Automattic\Jetpack\CRM\REST_API\V4;
use Automattic\Jetpack\CRM\Automation\Automation_Engine;
use Automattic\Jetpack\CRM\Automation\Automation_Exception;
use Automattic\Jetpack\CRM\Automation\Automation_Workflow;
use Automattic\Jetpack\CRM\Automation\Workflow\Workflow_Repository;
use Automattic\Jetpack\CRM\Automation\Workflow_Exception;
use Exception;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
defined( 'ABSPATH' ) || exit( 0 );
/**
* REST automation controller.
*
* @since 6.2.0
*/
final class REST_Automation_Workflows_Controller extends REST_Base_Controller {
/**
* The automation engine.
*
* @since 6.2.0
* @var Automation_Engine
*/
protected $automation_engine;
/**
* The workflow repository.
*
* @since 6.2.0
* @var Workflow_Repository
*/
protected $workflow_repository;
/**
* Constructor.
*
* @since 6.2.0
*/
public function __construct() {
parent::__construct();
$this->automation_engine = Automation_Engine::instance();
$this->workflow_repository = new Workflow_Repository();
$this->rest_base = 'automation';
}
/**
* Registers the routes for the objects of the controller.
*
* @since 6.2.0
* @see register_rest_route()
*
* @return void
*/
public function register_routes() {
// Register REST collection resource endpoints.
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/workflows',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'active' => array(
'description' => __( 'Whether to return only active workflows.', 'zero-bs-crm' ),
'type' => 'boolean',
),
'category' => array(
'description' => __( 'The category of the workflow.', 'zero-bs-crm' ),
'type' => 'string',
),
'page' => array(
'description' => __( 'The page of results to return.', 'zero-bs-crm' ),
'type' => 'integer',
'default' => 1,
),
'per_page' => array(
'description' => __( 'The amount of workflows to return per page.', 'zero-bs-crm' ),
'type' => 'integer',
'default' => 10,
// The min/max values are taken from the official documentation for the REST API.
// @link https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#minimum-and-maximum
'minimum' => 1,
'maximum' => 100,
),
'offset' => array(
'description' => __( 'The amount of workflows to offset the results by.', 'zero-bs-crm' ),
'type' => 'integer',
),
),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => $this->create_update_args( true ),
),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/workflows/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'zero-bs-crm' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => $this->create_update_args( false ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
)
);
}
/**
* Get all workflows.
*
* @since 6.2.0
*
* @param WP_REST_Request $request The request object.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
try {
$workflows = $this->workflow_repository->find_by(
$request->get_params(),
'id',
$this->get_per_page_argument( $request ),
$this->get_offset_argument( $request )
);
} catch ( Exception $e ) {
return new WP_Error(
'rest_unknown_error',
$e->getMessage(),
array( 'status' => 500 )
);
}
return rest_ensure_response( $this->prepare_items_for_response( $workflows, $request ) );
}
/**
* Get a single workflow.
*
* @since 6.2.0
*
* @param WP_REST_Request $request The request object.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
try {
$workflow = $this->workflow_repository->find( $request->get_param( 'id' ) );
if ( ! $workflow instanceof Automation_Workflow ) {
return new WP_Error(
'rest_invalid_workflow_id',
__( 'Invalid workflow ID.', 'zero-bs-crm' ),
array( 'status' => 404 )
);
}
} catch ( Exception $e ) {
return new WP_Error(
'rest_unknown_error',
$e->getMessage(),
array( 'status' => 500 )
);
}
return rest_ensure_response( $this->prepare_item_for_response( $workflow, $request ) );
}
/**
* Update a workflow.
*
* @since 6.2.0
*
* @param WP_REST_Request $request The request object.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
try {
$workflow = $this->prepare_item_for_database( $request );
if ( is_wp_error( $workflow ) ) {
return $workflow;
}
$this->workflow_repository->persist( $workflow );
} catch ( Workflow_Exception $e ) {
return new WP_Error(
'rest_workflow_exception',
$e->getMessage(),
array( 'status' => 500 )
);
} catch ( Exception $e ) {
return new WP_Error(
'rest_unknown_error',
$e->getMessage(),
array( 'status' => 500 )
);
}
return rest_ensure_response( $this->prepare_item_for_response( $workflow, $request ) );
}
/**
* Delete workflow.
*
* @since 6.2.0
*
* @param WP_REST_Request $request The request object.
* @return WP_Error|WP_REST_Response
*/
public function delete_item( $request ) {
try {
$workflow = $this->workflow_repository->find( $request->get_param( 'id' ) );
if ( ! $workflow instanceof Automation_Workflow ) {
return new WP_Error(
'rest_invalid_workflow_id',
__( 'Invalid workflow ID.', 'zero-bs-crm' ),
array( 'status' => 404 )
);
}
$this->workflow_repository->delete( $workflow );
} catch ( Workflow_Exception $e ) {
return new WP_Error(
'rest_workflow_exception',
$e->getMessage(),
array( 'status' => 500 )
);
} catch ( Exception $e ) {
return new WP_Error(
'rest_unknown_error',
$e->getMessage(),
array( 'status' => 500 )
);
}
return new WP_REST_Response( null, 204 );
}
/**
* Create workflow.
*
* @since 6.2.0
*
* @param WP_REST_Request $request The request object.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
try {
$workflow = $this->prepare_item_for_database( $request );
if ( is_wp_error( $workflow ) ) {
return $workflow;
}
$this->workflow_repository->persist( $workflow );
} catch ( Workflow_Exception $e ) {
return new WP_Error(
'rest_workflow_exception',
$e->getMessage(),
array( 'status' => 500 )
);
} catch ( Exception $e ) {
return new WP_Error(
'rest_unknown_error',
$e->getMessage(),
array( 'status' => 500 )
);
}
return rest_ensure_response( $this->prepare_item_for_response( $workflow, $request ) );
}
/**
* Checks if a given request has admin access to automations.
*
* @since 6.2.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the workflows, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$can_user_manage_workflows = zeroBSCRM_isZBSAdminOrAdmin();
if ( is_wp_error( $can_user_manage_workflows ) ) {
return $can_user_manage_workflows;
}
if ( $can_user_manage_workflows ) {
return true;
}
return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you cannot view this resource.', 'zero-bs-crm' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Prepares the workflow for the REST response.
*
* @since 6.2.0
*
* @param array $workflows WordPress' representation of the item.
* @param WP_REST_Request $request The request object.
* @return array A collection of workflow entities formatted as arrays.
*/
public function prepare_items_for_response( $workflows, $request ) {
foreach ( $workflows as $index => $workflow ) {
try {
$workflows[ $index ] = $this->prepare_item_for_response( $workflow, $request );
} catch ( Automation_Exception $e ) {
// @todo: Save the logs and show them in the UI. Continue preparing workflows skipping this workflow from the response.
continue;
}
}
return $workflows;
}
/**
* Prepares the workflow for the REST response.
*
* @since 6.2.0
*
* @param Automation_Workflow $workflow WordPress' representation of the item.
* @param WP_REST_Request $request The request object.
* @return array The workflow entity formatted as an array.
*/
public function prepare_item_for_response( $workflow, $request ) {
if ( $workflow instanceof Automation_Workflow ) {
$workflow = $workflow->to_array();
}
if ( is_array( $workflow['triggers'] ) ) {
foreach ( $workflow['triggers'] as $index => $trigger_slug ) {
$trigger_class = $this->automation_engine->get_trigger_class( $trigger_slug );
$hydrated_trigger = new $trigger_class();
$trigger_data = $hydrated_trigger::to_array();
$trigger_data['id'] = $index;
$workflow['triggers'][ $index ] = $trigger_data;
}
}
// Provide full context about steps (title, description, attribute definitions, etc.).
if ( is_array( $workflow['steps'] ) ) {
foreach ( $workflow['steps'] as $index => $step ) {
$hydrated_step = $this->automation_engine->get_registered_step( $step );
$step_array = $hydrated_step->to_array();
$step_array['id'] = $index;
$workflow['steps'][ $index ] = $step_array;
}
}
/**
* Filter individual workflow before returning the REST API response.
*
* @since 6.2.0
*
* @param array $workflow The workflow entity formatted as an array.
* @param WP_REST_Request $request The request object.
*/
return apply_filters( 'jpcrm_rest_prepare_workflows_item', $workflow, $request );
}
/**
* Get an array of supported arguments for POST/PUT endpoints.
*
* @since 6.2.0
*
* @param bool $create_workflow Whether we're creating a new workflow or not.
* @return array The supported arguments.
*/
protected function create_update_args( bool $create_workflow = false ): array {
return array(
'name' => array(
'description' => __( 'The name of the workflow.', 'zero-bs-crm' ),
'type' => 'string',
'required' => $create_workflow,
),
'description' => array(
'description' => __( 'A description of what the workflow does.', 'zero-bs-crm' ),
'type' => 'string',
'required' => $create_workflow,
),
'category' => array(
'description' => __( 'The category the workflow relates to.', 'zero-bs-crm' ),
'type' => 'string',
'required' => $create_workflow,
),
'active' => array(
'description' => __( 'Whether the workflow is active or not.', 'zero-bs-crm' ),
'type' => 'boolean',
'required' => $create_workflow,
),
'initial_step' => array(
'description' => __( 'The initial step of the workflow.', 'zero-bs-crm' ),
'type' => array( 'string', 'integer' ),
'required' => $create_workflow,
),
'steps' => array(
'description' => __( 'The steps of the workflow.', 'zero-bs-crm' ),
'type' => 'object',
'required' => $create_workflow,
'properties' => array(),
'additionalProperties' => array(
'type' => 'object',
'properties' => array(
'slug' => array(
'type' => 'string',
'required' => true,
),
),
),
),
);
}
/**
* Prepares one item for create or update operation.
*
* @since 6.2.0
*
* @param WP_REST_Request $request The request object.
* @return Automation_Workflow|WP_Error The workflow entity or a WP_Error if something went wrong.
*/
protected function prepare_item_for_database( $request ) {
// If we have an ID (e.g.: update request) we should fetch the existing workflow
// and update it, otherwise we should create a new one.
if ( $request->get_param( 'id' ) ) {
$workflow = $this->workflow_repository->find( $request->get_param( 'id' ) );
if ( ! $workflow instanceof Automation_Workflow ) {
return new WP_Error(
'rest_invalid_workflow_id',
__( 'Invalid workflow ID.', 'zero-bs-crm' ),
array( 'status' => 404 )
);
}
} else {
$workflow = new Automation_Workflow( array() );
}
foreach ( $request->get_params() as $param => $value ) {
switch ( $param ) {
case 'site':
$workflow->set_zbs_site( $value );
break;
case 'owner':
$workflow->set_zbs_owner( $value );
break;
case 'name':
$workflow->set_name( $value );
break;
case 'description':
$workflow->set_description( $value );
break;
case 'category':
$workflow->set_category( $value );
break;
case 'triggers':
$workflow->set_triggers( $value );
break;
case 'initial_step':
$workflow->set_initial_step( $value );
break;
case 'steps':
$workflow->set_steps( $value );
break;
case 'active':
if ( $value ) {
$workflow->turn_on();
} else {
$workflow->turn_off();
}
break;
}
}
return $workflow;
}
}