'integer', 'description' => __( 'The post or page ID.', 'aoedia' ), ); $output_schema = array( 'type' => 'object', 'properties' => array( 'id' => array( 'type' => 'integer' ), 'status' => array( 'type' => 'string' ), 'date' => array( 'type' => 'string' ), 'edit_url' => array( 'type' => 'string' ), ), ); wp_register_ability( 'aoedia/schedule-post', array( 'label' => __( 'Schedule Post', 'aoedia' ), 'description' => __( 'Schedules a draft or pending post or page to be published automatically at a future date, or changes the date of one already scheduled. The date is in the site\'s time zone, at least 5 minutes and at most 2 years ahead. Propose this only when the user asks for scheduling.', 'aoedia' ), 'category' => 'aoedia-content', 'input_schema' => array( 'type' => 'object', 'properties' => array( 'id' => $id_property, 'date' => array( 'type' => 'string', 'pattern' => '^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?$', 'description' => __( 'When to publish, as YYYY-MM-DD HH:MM in the site\'s time zone.', 'aoedia' ), ), ), 'required' => array( 'id', 'date' ), 'additionalProperties' => false, ), 'output_schema' => $output_schema, 'execute_callback' => 'aoedia_ability_schedule_post', 'permission_callback' => 'aoedia_can_publish_input', 'meta' => array( 'annotations' => array( 'readonly' => false, 'destructive' => false, 'idempotent' => true, ), 'show_in_rest' => true, ), ) ); wp_register_ability( 'aoedia/unschedule-post', array( 'label' => __( 'Unschedule Post', 'aoedia' ), 'description' => __( 'Turns a scheduled post or page back into a draft, so it is not published.', 'aoedia' ), 'category' => 'aoedia-content', 'input_schema' => array( 'type' => 'object', 'properties' => array( 'id' => $id_property ), 'required' => array( 'id' ), 'additionalProperties' => false, ), 'output_schema' => $output_schema, 'execute_callback' => 'aoedia_ability_unschedule_post', 'permission_callback' => 'aoedia_can_publish_input', 'meta' => array( 'annotations' => array( 'readonly' => false, 'destructive' => false, 'idempotent' => true, ), 'show_in_rest' => true, ), ) ); } /** * Returns whether the current user can edit and publish the post or page in the input. * * @param mixed $input The ability input. * @return bool Whether they can. */ function aoedia_can_publish_input( $input = array() ): bool { $post = get_post( is_array( $input ) && isset( $input['id'] ) ? (int) $input['id'] : 0 ); if ( ! $post || ! in_array( $post->post_type, array( 'post', 'page' ), true ) ) { return false; } return current_user_can( 'edit_post', $post->ID ) && current_user_can( get_post_type_object( $post->post_type )->cap->publish_posts ); } /** * Parses a schedule date in the site's time zone. * * @param string $date The date, as YYYY-MM-DD HH:MM[:SS]. * @return DateTimeImmutable|null The date, or null if it is not valid. */ function aoedia_parse_schedule_date( string $date ): ?DateTimeImmutable { $date = str_replace( 'T', ' ', $date ); $date = 16 === strlen( $date ) ? $date . ':00' : $date; $parsed = DateTimeImmutable::createFromFormat( '!Y-m-d H:i:s', $date, wp_timezone() ); // createFromFormat() rolls invalid dates such as 2026-02-31 over; refuse them instead. return $parsed && $parsed->format( 'Y-m-d H:i:s' ) === $date ? $parsed : null; } /** * Checks a scheduling change before it is proposed and again before it runs. * * @param string $ability The ability name. * @param array $input The validated input. * @return true|WP_Error True, or the problem. */ function aoedia_validate_schedule_change( string $ability, array $input ) { $post = get_post( (int) $input['id'] ); if ( ! $post ) { return new WP_Error( 'aoedia_post_not_found', __( 'Post not found.', 'aoedia' ) ); } if ( 'aoedia/unschedule-post' === $ability ) { return 'future' === $post->post_status ? true : new WP_Error( 'aoedia_not_scheduled', __( 'This is not scheduled.', 'aoedia' ) ); } if ( ! in_array( $post->post_status, array( 'draft', 'pending', 'future' ), true ) ) { return new WP_Error( 'aoedia_cannot_schedule', __( 'Only drafts, posts pending review and scheduled posts can be scheduled. This one is already published or private.', 'aoedia' ) ); } if ( '' === trim( $post->post_title ) ) { return new WP_Error( 'aoedia_cannot_schedule', __( 'Give it a title before scheduling it.', 'aoedia' ) ); } $date = aoedia_parse_schedule_date( (string) $input['date'] ); if ( ! $date ) { return new WP_Error( 'aoedia_invalid_date', __( 'The date is not valid. Use YYYY-MM-DD HH:MM.', 'aoedia' ) ); } $delay = $date->getTimestamp() - time(); if ( $delay < AOEDIA_SCHEDULE_MIN_DELAY ) { return new WP_Error( 'aoedia_invalid_date', __( 'The date must be at least 5 minutes in the future.', 'aoedia' ) ); } if ( $delay > AOEDIA_SCHEDULE_MAX_DELAY ) { return new WP_Error( 'aoedia_invalid_date', __( 'The date must be within the next 2 years.', 'aoedia' ) ); } return true; } /** * Returns the schedule date as people read it, for approval cards. * * @param string $date The date from the ability input. * @return string The formatted date, or an empty string if it is not valid. */ function aoedia_schedule_date_label( string $date ): string { $parsed = aoedia_parse_schedule_date( $date ); return $parsed ? wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $parsed->getTimestamp() ) : ''; } /** * Formats the result of a scheduling change. * * @param int $post_id The post ID. * @return array The result. */ function aoedia_schedule_result( int $post_id ): array { $post = get_post( $post_id ); return array( 'id' => $post_id, 'status' => $post->post_status, 'date' => $post->post_date, 'edit_url' => (string) get_edit_post_link( $post_id, 'raw' ), ); } /** * Executes aoedia/schedule-post. * * @param array $input The ability input. * @return array|WP_Error The scheduled post, or an error. */ function aoedia_ability_schedule_post( $input = array() ) { $check = aoedia_validate_schedule_change( 'aoedia/schedule-post', $input ); if ( is_wp_error( $check ) ) { return $check; } $date = aoedia_parse_schedule_date( (string) $input['date'] ); $result = wp_update_post( array( 'ID' => (int) $input['id'], 'post_status' => 'future', 'post_date' => $date->format( 'Y-m-d H:i:s' ), 'post_date_gmt' => $date->setTimezone( new DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' ), // Drafts keep a floating date unless told otherwise. 'edit_date' => true, ), true ); if ( is_wp_error( $result ) ) { return $result; } return aoedia_schedule_result( (int) $input['id'] ); } /** * Executes aoedia/unschedule-post. * * @param array $input The ability input. * @return array|WP_Error The post, now a draft, or an error. */ function aoedia_ability_unschedule_post( $input = array() ) { $check = aoedia_validate_schedule_change( 'aoedia/unschedule-post', $input ); if ( is_wp_error( $check ) ) { return $check; } $result = wp_update_post( array( 'ID' => (int) $input['id'], 'post_status' => 'draft', ), true ); if ( is_wp_error( $result ) ) { return $result; } return aoedia_schedule_result( (int) $input['id'] ); }