Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.80% covered (warning)
52.80%
179 / 339
40.00% covered (danger)
40.00%
8 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
Scheduled_Actions_Controller
52.82% covered (warning)
52.82%
178 / 337
40.00% covered (danger)
40.00%
8 / 20
465.72
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 register_routes
100.00% covered (success)
100.00%
83 / 83
100.00% covered (success)
100.00%
1 / 1
1
 get_item_schema
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
2
 basic_permissions_check
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 basic_post_permissions_check
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 get_items_permissions_check
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 get_items
21.05% covered (danger)
21.05%
4 / 19
0.00% covered (danger)
0.00%
0 / 1
11.87
 create_item_permissions_check
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
10
 create_item
12.50% covered (danger)
12.50%
4 / 32
0.00% covered (danger)
0.00%
0 / 1
21.75
 get_item_permissions_check
45.45% covered (danger)
45.45%
5 / 11
0.00% covered (danger)
0.00%
0 / 1
11.84
 get_item
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
2.21
 update_item_permissions_check
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 update_item
21.74% covered (danger)
21.74%
5 / 23
0.00% covered (danger)
0.00%
0 / 1
30.49
 delete_item_permissions_check
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete_item
31.25% covered (danger)
31.25%
5 / 16
0.00% covered (danger)
0.00%
0 / 1
9.20
 prepare_items_for_response
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 prepare_action_for_response
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 wpcom_get_action
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 format_date_for_output
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 format_date_for_db
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * The Publicize Scheduled Actions Controller class.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8namespace Automattic\Jetpack\Publicize\REST_API;
9
10use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
11use Automattic\Jetpack\Publicize\Connections;
12use Automattic\Jetpack\Publicize\Publicize_Utils as Utils;
13use WP_Error;
14use WP_REST_Request;
15use WP_REST_Response;
16use WP_REST_Server;
17
18if ( ! defined( 'ABSPATH' ) ) {
19    exit( 0 );
20}
21
22/**
23 * Scheduled Actions Controller class.
24 *
25 * @phan-constructor-used-for-side-effects
26 */
27class Scheduled_Actions_Controller extends Base_Controller {
28
29    use WPCOM_REST_API_Proxy_Request;
30
31    /**
32     * Constructor.
33     */
34    public function __construct() {
35        parent::__construct();
36
37        $this->base_api_path = 'wpcom';
38        $this->version       = 'v2';
39
40        $this->namespace = "{$this->base_api_path}/{$this->version}";
41        $this->rest_base = 'publicize/scheduled-actions';
42
43        add_action( 'rest_api_init', array( $this, 'register_routes' ) );
44    }
45
46    /**
47     * Register the routes.
48     */
49    public function register_routes() {
50        register_rest_route(
51            $this->namespace,
52            '/' . $this->rest_base,
53            array(
54                array(
55                    'methods'             => WP_REST_Server::READABLE,
56                    'callback'            => array( $this, 'get_items' ),
57                    'permission_callback' => array( $this, 'get_items_permissions_check' ),
58                    'args'                => array(
59                        'post_id' => array(
60                            'type'        => 'integer',
61                            'description' => __( 'The post ID to filter the items by.', 'jetpack-publicize-pkg' ),
62                        ),
63                    ),
64                ),
65                array(
66                    'methods'             => WP_REST_Server::CREATABLE,
67                    'callback'            => array( $this, 'create_item' ),
68                    'permission_callback' => array( $this, 'create_item_permissions_check' ),
69                    'args'                => array(
70                        'post_id'       => array(
71                            'type'     => 'integer',
72                            'required' => true,
73                        ),
74                        'connection_id' => array(
75                            'type'     => 'integer',
76                            'required' => true,
77                        ),
78                        'message'       => array(
79                            'type' => 'string',
80                        ),
81                        'share_date'    => array(
82                            'type'        => 'integer',
83                            'description' => sprintf(
84                                /* translators: %s is the new field name */
85                                __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
86                                'timestamp'
87                            ),
88                        ),
89                        'timestamp'     => array(
90                            'type'        => 'integer',
91                            'description' => __( 'GMT/UTC Unix timestamp in seconds for the action.', 'jetpack-publicize-pkg' ),
92                        ),
93                    ),
94                ),
95                'schema' => array( $this, 'get_public_item_schema' ),
96            )
97        );
98
99        register_rest_route(
100            $this->namespace,
101            '/' . $this->rest_base . '/(?P<action_id>\d+)',
102            array(
103                array(
104                    'methods'             => WP_REST_Server::READABLE,
105                    'callback'            => array( $this, 'get_item' ),
106                    'permission_callback' => array( $this, 'get_item_permissions_check' ),
107                ),
108                array(
109                    'methods'             => WP_REST_Server::EDITABLE,
110                    'callback'            => array( $this, 'update_item' ),
111                    'permission_callback' => array( $this, 'update_item_permissions_check' ),
112                    'args'                => array(
113                        'message'    => array( 'type' => 'string' ),
114                        'share_date' => array(
115                            'type'        => 'integer',
116                            'description' => sprintf(
117                                /* translators: %s is the new field name */
118                                __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
119                                'timestamp'
120                            ),
121                        ),
122                        'timestamp'  => array(
123                            'type'        => 'integer',
124                            'description' => __( 'GMT/UTC Unix timestamp in seconds for the action.', 'jetpack-publicize-pkg' ),
125                        ),
126                    ),
127                ),
128                array(
129                    'methods'             => WP_REST_Server::DELETABLE,
130                    'callback'            => array( $this, 'delete_item' ),
131                    'permission_callback' => array( $this, 'delete_item_permissions_check' ),
132                ),
133                'schema' => array( $this, 'get_public_item_schema' ),
134            )
135        );
136    }
137
138    /**
139     * Schema for the endpoint.
140     *
141     * @return array
142     */
143    public function get_item_schema() {
144        $schema = array(
145            '$schema'    => 'http://json-schema.org/draft-04/schema#',
146            'title'      => 'publicize-scheduled-action',
147            'type'       => 'object',
148            'properties' => array(
149                'blog_id'       => array(
150                    'type'        => 'integer',
151                    'description' => __( 'The blog ID that the action belongs to.', 'jetpack-publicize-pkg' ),
152                ),
153                'connection_id' => array(
154                    'type'        => 'integer',
155                    'description' => __( 'The publicize connection ID that the action belongs to.', 'jetpack-publicize-pkg' ),
156                ),
157                'id'            => array(
158                    'type'        => 'integer',
159                    'description' => __( 'Action identifier.', 'jetpack-publicize-pkg' ),
160                ),
161                'ID'            => array(
162                    'type'        => 'integer',
163                    'description' => __( 'Action identifier.', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
164                        /* translators: %s is the new field name */
165                        __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
166                        'id'
167                    ),
168                ),
169                'message'       => array(
170                    'type'        => 'string',
171                    'description' => __( 'The result of the action.', 'jetpack-publicize-pkg' ),
172                ),
173                'post_id'       => array(
174                    'type'        => 'integer',
175                    'description' => __( 'The post ID that the action belongs to.', 'jetpack-publicize-pkg' ),
176                ),
177                'share_date'    => array(
178                    'type'        => 'string',
179                    'description' => __( 'ISO 8601 formatted date for the action.', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
180                        /* translators: %s is the new field name */
181                        __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
182                        'timestamp'
183                    ),
184                ),
185                'timestamp'     => array(
186                    'type'        => 'integer',
187                    'description' => __( 'GMT/UTC Unix timestamp in seconds for the action.', 'jetpack-publicize-pkg' ),
188                ),
189                'wpcom_user_id' => array(
190                    'type'        => 'integer',
191                    'description' => __( 'wordpress.com ID of the user who created the action.', 'jetpack-publicize-pkg' ),
192                ),
193            ),
194        );
195
196        return $this->add_additional_fields_schema( $schema );
197    }
198
199    /**
200     * Check if the user has the basic permissions to access the Publicize scheduled actions.
201     *
202     * @return bool|WP_Error
203     */
204    public function basic_permissions_check() {
205        if ( ! current_user_can( 'edit_posts' ) ) {
206            return false;
207        }
208        return $this->publicize_permissions_check();
209    }
210
211    /**
212     * Check if the user has the basic permissions
213     * required to perform CRUD operations on an item related to a post
214     *
215     * @param int $post_id The post ID.
216     * @return bool|WP_Error
217     */
218    public function basic_post_permissions_check( $post_id ) {
219
220        if ( ! get_post( $post_id ) ) {
221            return new WP_Error(
222                'post_not_found',
223                __( 'Post not found.', 'jetpack-publicize-pkg' ),
224                array( 'status' => 400 )
225            );
226        }
227
228        // Ensure that the user can edit the post.
229        if ( ! current_user_can( 'edit_post', $post_id ) ) {
230            return new WP_Error(
231                'rest_forbidden',
232                __( 'Sorry, you are not allowed to view or scheduled shares for that post.', 'jetpack-publicize-pkg' ),
233                array( 'status' => 403 )
234            );
235        }
236
237        return true;
238    }
239
240    /**
241     * Verify that the request has access to connectoins list.
242     *
243     * @param WP_REST_Request $request Full details about the request.
244     * @return bool|WP_Error
245     */
246    public function get_items_permissions_check( $request ) {
247        $basic_permissions = $this->basic_permissions_check();
248
249        if ( is_wp_error( $basic_permissions ) || ! $basic_permissions ) {
250            return $basic_permissions;
251        }
252
253        $post_id = $request->get_param( 'post_id' );
254
255        /**
256         * The post_id is optional only for editors and above.
257         * It means that authors can view the scheduled shares
258         * only for the post they can edit but
259         * cannot view all the scheduled shares for the site.
260         */
261        if ( ! $post_id && ! current_user_can( 'edit_others_posts' ) ) {
262            return new WP_Error(
263                'rest_forbidden',
264                __( 'You must pass a post ID to list scheduled shares.', 'jetpack-publicize-pkg' ),
265                array( 'status' => rest_authorization_required_code() )
266            );
267        }
268
269        if ( $post_id ) {
270            return $this->basic_post_permissions_check( $post_id );
271        }
272
273        return true;
274    }
275
276    /**
277     * Get list of Publicize scheduled actions
278     *
279     * @param WP_REST_Request $request Full details about the request.
280     *
281     * @return WP_REST_Response|WP_Error The response
282     */
283    public function get_items( $request ) {
284
285        if ( Utils::is_wpcom() ) {
286            $post_id = $request->get_param( 'post_id' );
287
288            require_lib( 'publicize/class.publicize-actions' );
289
290            if ( $post_id ) {
291                $scheduled_actions = \Publicize_Actions::get_scheduled_actions_by_blog_and_post_id(
292                    get_current_blog_id(),
293                    $post_id
294                );
295            } else {
296                $scheduled_actions = \Publicize_Actions::get_scheduled_actions_by_blog_id(
297                    get_current_blog_id()
298                );
299            }
300
301            if ( is_wp_error( $scheduled_actions ) ) {
302                return $scheduled_actions;
303            }
304
305            return rest_ensure_response(
306                $this->prepare_items_for_response( $scheduled_actions, $request )
307            );
308        }
309
310        return rest_ensure_response(
311            $this->proxy_request_to_wpcom_as_user( $request )
312        );
313    }
314
315    /**
316     * Checks if a given request has access to create a connection.
317     *
318     * @param WP_REST_Request $request Full details about the request.
319     * @return bool|WP_Error True if the request has access to create items, WP_Error object otherwise.
320     */
321    public function create_item_permissions_check( $request ) {
322        $basic_permissions = $this->basic_permissions_check();
323
324        if ( is_wp_error( $basic_permissions ) || ! $basic_permissions ) {
325            return $basic_permissions;
326        }
327
328        $post_id = $request->get_param( 'post_id' );
329
330        $basic_post_permissions = $this->basic_post_permissions_check( $post_id );
331
332        if ( is_wp_error( $basic_post_permissions ) || ! $basic_post_permissions ) {
333            return $basic_post_permissions;
334        }
335
336        $post = get_post( $post_id );
337
338        // Ensure that the post is published.
339        if ( 'publish' !== $post->post_status ) {
340            return new WP_Error(
341                'post_not_published',
342                __( 'The post must be published to schedule it for sharing.', 'jetpack-publicize-pkg' ),
343                array( 'status' => 400 )
344            );
345        }
346
347        /**
348         * We need to validate the passed connection_id
349         * to ensure that it's valid and the user has access to the connection.
350         */
351        $connection = Connections::get_by_id( (string) $request->get_param( 'connection_id' ) );
352
353        if ( ! $connection ) {
354            return new WP_Error(
355                'connection_not_found',
356                __( 'That connection does not exist.', 'jetpack-publicize-pkg' ),
357                array( 'status' => 400 )
358            );
359        }
360
361        if ( current_user_can( 'edit_others_posts' ) ) {
362            return true;
363        }
364
365        /**
366         * If the user is not an editor or above, they can create
367         * actions only for the connections they have access to.
368         * So, we need to check if the user has access to the connection
369         * that they are trying to use to create the action.
370         */
371        if ( ! Connections::is_shared( $connection ) && ! Connections::user_owns_connection( $connection ) ) {
372            return new WP_Error(
373                'rest_forbidden',
374                __( 'Sorry, you cannot schedule shares to that connection.', 'jetpack-publicize-pkg' ),
375                array( 'status' => 403 )
376            );
377        }
378
379        return true;
380    }
381
382    /**
383     * Creates a new action.
384     *
385     * @param WP_REST_Request $request Full details about the request.
386     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
387     */
388    public function create_item( $request ) {
389
390        if ( Utils::is_wpcom() ) {
391            require_lib( 'publicize/class.publicize-actions' );
392
393            $post_id       = $request['post_id'];
394            $blog_id       = get_current_blog_id();
395            $user_id       = get_current_user_id();
396            $connection_id = (int) $request['connection_id'];
397            $message       = sanitize_textarea_field( $request['message'] ?? '' );
398
399            $timestamp = time();
400            if ( ! empty( $request['timestamp'] ) ) {
401                $timestamp = (int) $request['timestamp'];
402            } elseif ( ! empty( $request['share_date'] ) ) { // Fallback for deprecated field.
403                $timestamp = $request['share_date']; // Calypso sends this as timestamp.
404            }
405
406            $action = array(
407                'post_id'            => $post_id,
408                'blog_id'            => $blog_id,
409                'user_id'            => $user_id,
410                'connection_id'      => $connection_id,
411                'message'            => $message,
412                'scheduled_datetime' => $this->format_date_for_db( $timestamp ),
413            );
414
415            $action_id = \Publicize_Actions::add_scheduled_action( $action );
416            if ( is_wp_error( $action_id ) ) {
417                return $action_id;
418            }
419            $action['publicize_scheduled_action_id'] = $action_id;
420
421            $response = rest_ensure_response(
422                $this->prepare_action_for_response( $action )
423            );
424
425            $response->set_status( 201 );
426
427            return $response;
428        }
429
430        return rest_ensure_response(
431            $this->proxy_request_to_wpcom_as_user( $request )
432        );
433    }
434
435    /**
436     * Checks if a given request has access to read an action.
437     *
438     * @param WP_REST_Request $request Full details about the request.
439     * @return bool|WP_Error True if the request has read access for the item, WP_Error object or false otherwise.
440     */
441    public function get_item_permissions_check( $request ) {
442        $basic_permissions = $this->basic_permissions_check();
443
444        if ( is_wp_error( $basic_permissions ) || ! $basic_permissions ) {
445            return $basic_permissions;
446        }
447
448        if ( ! Utils::is_wpcom() ) {
449            // On Jetpack sites, we need to just check for basic permissions.
450            return true;
451        }
452
453        $action = $this->wpcom_get_action( $request['action_id'] );
454
455        if ( is_wp_error( $action ) ) {
456            return $action;
457        }
458
459        // Ensure that the action is for the current blog.
460        if ( get_current_blog_id() !== $action['blog_id'] ) {
461            return false;
462        }
463
464        return $this->basic_post_permissions_check( $action['post_id'] );
465    }
466
467    /**
468     * Retrieves a single action.
469     *
470     * @param WP_REST_Request $request Full details about the request.
471     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
472     */
473    public function get_item( $request ) {
474        $action_id = $request['action_id'];
475
476        if ( Utils::is_wpcom() ) {
477
478            return rest_ensure_response(
479                $this->wpcom_get_action( $action_id )
480            );
481        }
482
483        return rest_ensure_response(
484            $this->proxy_request_to_wpcom_as_user( $request, $action_id )
485        );
486    }
487
488    /**
489     * Checks if a given request has access to update an action.
490     *
491     * @param WP_REST_Request $request Full details about the request.
492     * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
493     */
494    public function update_item_permissions_check( $request ) {
495        // If a user can view an item, they can update it.
496        return $this->get_item_permissions_check( $request );
497    }
498
499    /**
500     * Update an action.
501     *
502     * @param WP_REST_Request $request Full details about the request.
503     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
504     */
505    public function update_item( $request ) {
506
507        $action_id = $request['action_id'];
508
509        if ( Utils::is_wpcom() ) {
510            require_lib( 'publicize/class.publicize-actions' );
511
512            $action = $this->wpcom_get_action( $action_id );
513
514            if ( is_wp_error( $action ) ) {
515                return $action;
516            }
517            $action['message'] = ! empty( $request['message'] ) ? sanitize_textarea_field( $request['message'] ) : $action['message'];
518
519            // Retain the original timestamp by default.
520            $timestamp = $action['timestamp'];
521            if ( ! empty( $request['timestamp'] ) ) {
522                $timestamp = (int) $request['timestamp'];
523            } elseif ( ! empty( $request['share_date'] ) ) { // Fallback for deprecated field.
524                $timestamp = strtotime( $request['share_date'] );
525            }
526            $action['scheduled_datetime'] = $this->format_date_for_db( $timestamp );
527
528            $action['publicize_scheduled_action_id'] = $action['id'];
529
530            $save_result = \Publicize_Actions::edit_scheduled_action( $action['id'], $action );
531            if ( is_wp_error( $save_result ) ) {
532                return $save_result;
533            }
534            return rest_ensure_response(
535                $this->prepare_action_for_response( $action )
536            );
537        }
538
539        return rest_ensure_response(
540            $this->proxy_request_to_wpcom_as_user( $request, $action_id )
541        );
542    }
543
544    /**
545     * Checks if a given request has access to delete an action.
546     *
547     * @param WP_REST_Request $request Full details about the request.
548     * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
549     */
550    public function delete_item_permissions_check( $request ) {
551        // If a user can update an item, they can delete it.
552        return $this->update_item_permissions_check( $request );
553    }
554
555    /**
556     * Delete an action.
557     *
558     * @param WP_REST_Request $request Full details about the request.
559     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
560     */
561    public function delete_item( $request ) {
562
563        $action_id = $request['action_id'];
564
565        if ( Utils::is_wpcom() ) {
566            require_lib( 'publicize/class.publicize-actions' );
567
568            $action = $this->wpcom_get_action( $action_id );
569            if ( is_wp_error( $action ) ) {
570                return $action;
571            }
572            $delete_result = \Publicize_Actions::delete_scheduled_action(
573                $action['id'],
574                $action['blog_id']
575            );
576            if ( is_wp_error( $delete_result ) ) {
577                return $delete_result;
578            }
579            return rest_ensure_response( true );
580        }
581
582        return rest_ensure_response(
583            $this->proxy_request_to_wpcom_as_user( $request, $action_id )
584        );
585    }
586
587    /**
588     * Filters out data based on ?_fields= request parameter
589     *
590     * @param array           $items   Items to prepare.
591     * @param WP_REST_Request $request Full details about the request.
592     *
593     * @return array Items.
594     */
595    public function prepare_items_for_response( $items, $request ) {
596
597        $output = array();
598
599        foreach ( $items as $raw_item ) {
600
601            $item = $this->prepare_action_for_response( $raw_item );
602
603            $data = $this->prepare_item_for_response( $item, $request );
604
605            $output[] = $this->prepare_response_for_collection( $data );
606        }
607
608        return $output;
609    }
610
611    /**
612     * Prepare a single action for response, setting the correct field names.
613     *
614     * @param array $raw_action Raw action.
615     *
616     * @return array Items.
617     */
618    public function prepare_action_for_response( $raw_action ) {
619
620        return array(
621            'blog_id'       => (int) $raw_action['blog_id'],
622            'connection_id' => (int) $raw_action['connection_id'],
623            'id'            => (int) $raw_action['publicize_scheduled_action_id'],
624            'ID'            => (int) $raw_action['publicize_scheduled_action_id'],
625            'message'       => (string) $raw_action['message'],
626            'post_id'       => (int) $raw_action['post_id'],
627            'share_date'    => (string) $this->format_date_for_output( $raw_action['scheduled_datetime'] ),
628            'timestamp'     => strtotime( $raw_action['scheduled_datetime'] ),
629            'wpcom_user_id' => (int) $raw_action['user_id'],
630        );
631    }
632
633    /**
634     * Return a formatted action by action_id
635     *
636     * @param int $action_id The action ID.
637     * @return WP_Error|array The action
638     */
639    private function wpcom_get_action( $action_id ) {
640        // Ensure that we are on WPCOM.
641        Utils::assert_is_wpcom( __METHOD__ );
642
643        require_lib( 'publicize/class.publicize-actions' );
644        $action = \Publicize_Actions::get_scheduled_action( $action_id );
645        if ( is_wp_error( $action ) ) {
646            return $action;
647        }
648        if ( ! isset( $action['publicize_scheduled_action_id'] ) ) {
649            return new WP_Error( 'not_found', __( 'Could not find that scheduled action.', 'jetpack-publicize-pkg' ), array( 'status' => 404 ) );
650        }
651
652        return $this->prepare_action_for_response( $action );
653    }
654
655    /**
656     * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
657     *
658     * @param string $date_gmt GMT datetime string.
659     * @return string
660     */
661    private function format_date_for_output( $date_gmt ) {
662        // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
663        return date( 'c', strtotime( $date_gmt ) );
664    }
665
666    /**
667     * Returns SQL formatted datetime from unix timestamp
668     *
669     * @param int $timestamp The timestamp.
670     *
671     * @return string
672     */
673    private function format_date_for_db( $timestamp ) {
674        // Round down to the nearest minute.
675        $floored_timestamp = $timestamp - $timestamp % 60;
676        return gmdate( 'Y-m-d H:i:s', $floored_timestamp );
677    }
678}