Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
31.73% covered (danger)
31.73%
86 / 271
8.33% covered (danger)
8.33%
1 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Automation_Workflows_Controller
31.85% covered (danger)
31.85%
86 / 270
8.33% covered (danger)
8.33%
1 / 12
715.70
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 register_routes
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 1
2
 get_items
53.85% covered (warning)
53.85%
7 / 13
0.00% covered (danger)
0.00%
0 / 1
2.39
 get_item
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
3.71
 update_item
23.53% covered (danger)
23.53%
4 / 17
0.00% covered (danger)
0.00%
0 / 1
11.15
 delete_item
19.05% covered (danger)
19.05%
4 / 21
0.00% covered (danger)
0.00%
0 / 1
12.49
 create_item
23.53% covered (danger)
23.53%
4 / 17
0.00% covered (danger)
0.00%
0 / 1
11.15
 get_item_permissions_check
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
 prepare_items_for_response
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 prepare_item_for_response
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 create_update_args
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
2
 prepare_item_for_database
77.50% covered (warning)
77.50%
31 / 40
0.00% covered (danger)
0.00%
0 / 1
16.23
1<?php
2/**
3 * Automation REST controller.
4 *
5 * @package automattic/jetpack-crm
6 */
7
8namespace Automattic\Jetpack\CRM\REST_API\V4;
9
10use Automattic\Jetpack\CRM\Automation\Automation_Engine;
11use Automattic\Jetpack\CRM\Automation\Automation_Exception;
12use Automattic\Jetpack\CRM\Automation\Automation_Workflow;
13use Automattic\Jetpack\CRM\Automation\Workflow\Workflow_Repository;
14use Automattic\Jetpack\CRM\Automation\Workflow_Exception;
15use Exception;
16use WP_Error;
17use WP_REST_Request;
18use WP_REST_Response;
19use WP_REST_Server;
20
21defined( 'ABSPATH' ) || exit( 0 );
22
23/**
24 * REST automation controller.
25 *
26 * @since 6.2.0
27 */
28final class REST_Automation_Workflows_Controller extends REST_Base_Controller {
29
30    /**
31     * The automation engine.
32     *
33     * @since 6.2.0
34     * @var Automation_Engine
35     */
36    protected $automation_engine;
37
38    /**
39     * The workflow repository.
40     *
41     * @since 6.2.0
42     * @var Workflow_Repository
43     */
44    protected $workflow_repository;
45
46    /**
47     * Constructor.
48     *
49     * @since 6.2.0
50     */
51    public function __construct() {
52        parent::__construct();
53
54        $this->automation_engine   = Automation_Engine::instance();
55        $this->workflow_repository = new Workflow_Repository();
56        $this->rest_base           = 'automation';
57    }
58
59    /**
60     * Registers the routes for the objects of the controller.
61     *
62     * @since 6.2.0
63     * @see register_rest_route()
64     *
65     * @return void
66     */
67    public function register_routes() {
68        // Register REST collection resource endpoints.
69        register_rest_route(
70            $this->namespace,
71            '/' . $this->rest_base . '/workflows',
72            array(
73                array(
74                    'methods'             => WP_REST_Server::READABLE,
75                    'callback'            => array( $this, 'get_items' ),
76                    'permission_callback' => array( $this, 'get_item_permissions_check' ),
77                    'args'                => array(
78                        'active'   => array(
79                            'description' => __( 'Whether to return only active workflows.', 'zero-bs-crm' ),
80                            'type'        => 'boolean',
81                        ),
82                        'category' => array(
83                            'description' => __( 'The category of the workflow.', 'zero-bs-crm' ),
84                            'type'        => 'string',
85                        ),
86                        'page'     => array(
87                            'description' => __( 'The page of results to return.', 'zero-bs-crm' ),
88                            'type'        => 'integer',
89                            'default'     => 1,
90                        ),
91                        'per_page' => array(
92                            'description' => __( 'The amount of workflows to return per page.', 'zero-bs-crm' ),
93                            'type'        => 'integer',
94                            'default'     => 10,
95                            // The min/max values are taken from the official documentation for the REST API.
96                            // @link https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#minimum-and-maximum
97                            'minimum'     => 1,
98                            'maximum'     => 100,
99                        ),
100                        'offset'   => array(
101                            'description' => __( 'The amount of workflows to offset the results by.', 'zero-bs-crm' ),
102                            'type'        => 'integer',
103                        ),
104                    ),
105                ),
106                array(
107                    'methods'             => WP_REST_Server::CREATABLE,
108                    'callback'            => array( $this, 'create_item' ),
109                    'permission_callback' => array( $this, 'get_item_permissions_check' ),
110                    'args'                => $this->create_update_args( true ),
111                ),
112            )
113        );
114
115        register_rest_route(
116            $this->namespace,
117            '/' . $this->rest_base . '/workflows/(?P<id>[\d]+)',
118            array(
119                'args' => array(
120                    'id' => array(
121                        'description' => __( 'Unique identifier for the resource.', 'zero-bs-crm' ),
122                        'type'        => 'integer',
123                    ),
124                ),
125                array(
126                    'methods'             => WP_REST_Server::READABLE,
127                    'callback'            => array( $this, 'get_item' ),
128                    'permission_callback' => array( $this, 'get_item_permissions_check' ),
129                ),
130                array(
131                    'methods'             => WP_REST_Server::EDITABLE,
132                    'callback'            => array( $this, 'update_item' ),
133                    'permission_callback' => array( $this, 'get_item_permissions_check' ),
134                    'args'                => $this->create_update_args( false ),
135                ),
136                array(
137                    'methods'             => WP_REST_Server::DELETABLE,
138                    'callback'            => array( $this, 'delete_item' ),
139                    'permission_callback' => array( $this, 'get_item_permissions_check' ),
140                ),
141            )
142        );
143    }
144
145    /**
146     * Get all workflows.
147     *
148     * @since 6.2.0
149     *
150     * @param WP_REST_Request $request The request object.
151     * @return WP_Error|WP_REST_Response
152     */
153    public function get_items( $request ) {
154        try {
155            $workflows = $this->workflow_repository->find_by(
156                $request->get_params(),
157                'id',
158                $this->get_per_page_argument( $request ),
159                $this->get_offset_argument( $request )
160            );
161        } catch ( Exception $e ) {
162            return new WP_Error(
163                'rest_unknown_error',
164                $e->getMessage(),
165                array( 'status' => 500 )
166            );
167        }
168
169        return rest_ensure_response( $this->prepare_items_for_response( $workflows, $request ) );
170    }
171
172    /**
173     * Get a single workflow.
174     *
175     * @since 6.2.0
176     *
177     * @param WP_REST_Request $request The request object.
178     * @return WP_Error|WP_REST_Response
179     */
180    public function get_item( $request ) {
181        try {
182            $workflow = $this->workflow_repository->find( $request->get_param( 'id' ) );
183
184            if ( ! $workflow instanceof Automation_Workflow ) {
185                return new WP_Error(
186                    'rest_invalid_workflow_id',
187                    __( 'Invalid workflow ID.', 'zero-bs-crm' ),
188                    array( 'status' => 404 )
189                );
190            }
191        } catch ( Exception $e ) {
192            return new WP_Error(
193                'rest_unknown_error',
194                $e->getMessage(),
195                array( 'status' => 500 )
196            );
197        }
198
199        return rest_ensure_response( $this->prepare_item_for_response( $workflow, $request ) );
200    }
201
202    /**
203     * Update a workflow.
204     *
205     * @since 6.2.0
206     *
207     * @param WP_REST_Request $request The request object.
208     * @return WP_Error|WP_REST_Response
209     */
210    public function update_item( $request ) {
211        try {
212            $workflow = $this->prepare_item_for_database( $request );
213
214            if ( is_wp_error( $workflow ) ) {
215                return $workflow;
216            }
217
218            $this->workflow_repository->persist( $workflow );
219        } catch ( Workflow_Exception $e ) {
220            return new WP_Error(
221                'rest_workflow_exception',
222                $e->getMessage(),
223                array( 'status' => 500 )
224            );
225        } catch ( Exception $e ) {
226            return new WP_Error(
227                'rest_unknown_error',
228                $e->getMessage(),
229                array( 'status' => 500 )
230            );
231        }
232
233        return rest_ensure_response( $this->prepare_item_for_response( $workflow, $request ) );
234    }
235
236    /**
237     * Delete workflow.
238     *
239     * @since 6.2.0
240     *
241     * @param WP_REST_Request $request The request object.
242     * @return WP_Error|WP_REST_Response
243     */
244    public function delete_item( $request ) {
245        try {
246            $workflow = $this->workflow_repository->find( $request->get_param( 'id' ) );
247
248            if ( ! $workflow instanceof Automation_Workflow ) {
249                return new WP_Error(
250                    'rest_invalid_workflow_id',
251                    __( 'Invalid workflow ID.', 'zero-bs-crm' ),
252                    array( 'status' => 404 )
253                );
254            }
255
256            $this->workflow_repository->delete( $workflow );
257        } catch ( Workflow_Exception $e ) {
258            return new WP_Error(
259                'rest_workflow_exception',
260                $e->getMessage(),
261                array( 'status' => 500 )
262            );
263        } catch ( Exception $e ) {
264            return new WP_Error(
265                'rest_unknown_error',
266                $e->getMessage(),
267                array( 'status' => 500 )
268            );
269        }
270
271        return new WP_REST_Response( null, 204 );
272    }
273
274    /**
275     * Create workflow.
276     *
277     * @since 6.2.0
278     *
279     * @param WP_REST_Request $request The request object.
280     * @return WP_Error|WP_REST_Response
281     */
282    public function create_item( $request ) {
283        try {
284            $workflow = $this->prepare_item_for_database( $request );
285
286            if ( is_wp_error( $workflow ) ) {
287                return $workflow;
288            }
289
290            $this->workflow_repository->persist( $workflow );
291        } catch ( Workflow_Exception $e ) {
292            return new WP_Error(
293                'rest_workflow_exception',
294                $e->getMessage(),
295                array( 'status' => 500 )
296            );
297        } catch ( Exception $e ) {
298            return new WP_Error(
299                'rest_unknown_error',
300                $e->getMessage(),
301                array( 'status' => 500 )
302            );
303        }
304
305        return rest_ensure_response( $this->prepare_item_for_response( $workflow, $request ) );
306    }
307
308    /**
309     * Checks if a given request has admin access to automations.
310     *
311     * @since 6.2.0
312     *
313     * @param WP_REST_Request $request Full details about the request.
314     * @return true|WP_Error True if the request has read access for the workflows, WP_Error object otherwise.
315     */
316    public function get_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
317        $can_user_manage_workflows = zeroBSCRM_isZBSAdminOrAdmin();
318
319        if ( is_wp_error( $can_user_manage_workflows ) ) {
320            return $can_user_manage_workflows;
321        }
322
323        if ( $can_user_manage_workflows ) {
324            return true;
325        }
326
327        return new WP_Error(
328            'rest_cannot_view',
329            __( 'Sorry, you cannot view this resource.', 'zero-bs-crm' ),
330            array( 'status' => rest_authorization_required_code() )
331        );
332    }
333
334    /**
335     * Prepares the workflow for the REST response.
336     *
337     * @since 6.2.0
338     *
339     * @param array           $workflows WordPress' representation of the item.
340     * @param WP_REST_Request $request The request object.
341     * @return array A collection of workflow entities formatted as arrays.
342     */
343    public function prepare_items_for_response( $workflows, $request ) {
344        foreach ( $workflows as $index => $workflow ) {
345            try {
346                $workflows[ $index ] = $this->prepare_item_for_response( $workflow, $request );
347            } catch ( Automation_Exception $e ) {
348                // @todo: Save the logs and show them in the UI. Continue preparing workflows skipping this workflow from the response.
349                continue;
350            }
351        }
352
353        return $workflows;
354    }
355
356    /**
357     * Prepares the workflow for the REST response.
358     *
359     * @since 6.2.0
360     *
361     * @param Automation_Workflow $workflow WordPress' representation of the item.
362     * @param WP_REST_Request     $request The request object.
363     * @return array The workflow entity formatted as an array.
364     */
365    public function prepare_item_for_response( $workflow, $request ) {
366        if ( $workflow instanceof Automation_Workflow ) {
367            $workflow = $workflow->to_array();
368        }
369
370        if ( is_array( $workflow['triggers'] ) ) {
371            foreach ( $workflow['triggers'] as $index => $trigger_slug ) {
372                $trigger_class                  = $this->automation_engine->get_trigger_class( $trigger_slug );
373                $hydrated_trigger               = new $trigger_class();
374                $trigger_data                   = $hydrated_trigger::to_array();
375                $trigger_data['id']             = $index;
376                $workflow['triggers'][ $index ] = $trigger_data;
377            }
378        }
379
380        // Provide full context about steps (title, description, attribute definitions, etc.).
381        if ( is_array( $workflow['steps'] ) ) {
382            foreach ( $workflow['steps'] as $index => $step ) {
383                $hydrated_step               = $this->automation_engine->get_registered_step( $step );
384                $step_array                  = $hydrated_step->to_array();
385                $step_array['id']            = $index;
386                $workflow['steps'][ $index ] = $step_array;
387            }
388        }
389
390        /**
391         * Filter individual workflow before returning the REST API response.
392         *
393         * @since 6.2.0
394         *
395         * @param array           $workflow The workflow entity formatted as an array.
396         * @param WP_REST_Request $request The request object.
397         */
398        return apply_filters( 'jpcrm_rest_prepare_workflows_item', $workflow, $request );
399    }
400
401    /**
402     * Get an array of supported arguments for POST/PUT endpoints.
403     *
404     * @since 6.2.0
405     *
406     * @param bool $create_workflow Whether we're creating a new workflow or not.
407     * @return array The supported arguments.
408     */
409    protected function create_update_args( bool $create_workflow = false ): array {
410        return array(
411            'name'         => array(
412                'description' => __( 'The name of the workflow.', 'zero-bs-crm' ),
413                'type'        => 'string',
414                'required'    => $create_workflow,
415            ),
416            'description'  => array(
417                'description' => __( 'A description of what the workflow does.', 'zero-bs-crm' ),
418                'type'        => 'string',
419                'required'    => $create_workflow,
420            ),
421            'category'     => array(
422                'description' => __( 'The category the workflow relates to.', 'zero-bs-crm' ),
423                'type'        => 'string',
424                'required'    => $create_workflow,
425            ),
426            'active'       => array(
427                'description' => __( 'Whether the workflow is active or not.', 'zero-bs-crm' ),
428                'type'        => 'boolean',
429                'required'    => $create_workflow,
430            ),
431            'initial_step' => array(
432                'description' => __( 'The initial step of the workflow.', 'zero-bs-crm' ),
433                'type'        => array( 'string', 'integer' ),
434                'required'    => $create_workflow,
435            ),
436            'steps'        => array(
437                'description'          => __( 'The steps of the workflow.', 'zero-bs-crm' ),
438                'type'                 => 'object',
439                'required'             => $create_workflow,
440                'properties'           => array(),
441                'additionalProperties' => array(
442                    'type'       => 'object',
443                    'properties' => array(
444                        'slug' => array(
445                            'type'     => 'string',
446                            'required' => true,
447                        ),
448                    ),
449                ),
450            ),
451        );
452    }
453
454    /**
455     * Prepares one item for create or update operation.
456     *
457     * @since 6.2.0
458     *
459     * @param WP_REST_Request $request The request object.
460     * @return Automation_Workflow|WP_Error The workflow entity or a WP_Error if something went wrong.
461     */
462    protected function prepare_item_for_database( $request ) {
463        // If we have an ID (e.g.: update request) we should fetch the existing workflow
464        // and update it, otherwise we should create a new one.
465        if ( $request->get_param( 'id' ) ) {
466            $workflow = $this->workflow_repository->find( $request->get_param( 'id' ) );
467
468            if ( ! $workflow instanceof Automation_Workflow ) {
469                return new WP_Error(
470                    'rest_invalid_workflow_id',
471                    __( 'Invalid workflow ID.', 'zero-bs-crm' ),
472                    array( 'status' => 404 )
473                );
474            }
475        } else {
476            $workflow = new Automation_Workflow( array() );
477        }
478
479        foreach ( $request->get_params() as $param => $value ) {
480            switch ( $param ) {
481                case 'site':
482                    $workflow->set_zbs_site( $value );
483                    break;
484
485                case 'owner':
486                    $workflow->set_zbs_owner( $value );
487                    break;
488
489                case 'name':
490                    $workflow->set_name( $value );
491                    break;
492
493                case 'description':
494                    $workflow->set_description( $value );
495                    break;
496
497                case 'category':
498                    $workflow->set_category( $value );
499                    break;
500
501                case 'triggers':
502                    $workflow->set_triggers( $value );
503                    break;
504
505                case 'initial_step':
506                    $workflow->set_initial_step( $value );
507                    break;
508
509                case 'steps':
510                    $workflow->set_steps( $value );
511                    break;
512
513                case 'active':
514                    if ( $value ) {
515                        $workflow->turn_on();
516                    } else {
517                        $workflow->turn_off();
518                    }
519                    break;
520            }
521        }
522
523        return $workflow;
524    }
525}