Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions class-wp-press-this-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -1552,6 +1552,8 @@ public function html() {
'postId' => $post_ID,
'title' => $post_title,
'content' => $post_content,
'postStatus' => $post->post_status,
'postDate' => mysql_to_rfc3339( $post->post_date ),
'nonce' => wp_create_nonce( 'update-post_' . $post_ID ),
'categoryNonce' => wp_create_nonce( 'add-category' ),

Expand Down Expand Up @@ -1615,6 +1617,7 @@ public function html() {
// Config.
'redirInParent' => $site_settings['redirInParent'],
'isRTL' => is_rtl(),
'timezone' => wp_timezone_string(),

// Allowed blocks.
'allowedBlocks' => $this->get_allowed_blocks(),
Expand Down
1 change: 1 addition & 0 deletions includes/class-press-this-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ public function get_script_dependencies() {
'wp-format-library',
'wp-html-entities',
'wp-i18n',
'wp-keyboard-shortcuts',
'wp-primitives',
);
}
Expand Down
52 changes: 51 additions & 1 deletion press-this-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,29 @@ function press_this_register_rest_routes() {
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => 'draft',
'enum' => array( 'draft', 'publish' ),
'enum' => array( 'draft', 'publish', 'future' ),
),
'date' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
// Validates format only (ISO 8601 datetime). Semantic validation
// (e.g. month/day ranges, leap years) is handled server-side by
// strtotime() in the save handler, which rejects unparseable
// dates and normalizes edge cases like Feb 30 → Mar 2.
'validate_callback' => function ( $value ) {
if ( empty( $value ) ) {
return true;
}
if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?$/', $value ) ) {
return new WP_Error(
'press_this_invalid_date_format',
__( 'Date must be in ISO 8601 format.', 'press-this' ),
array( 'status' => 400 )
);
}
return true;
},
Comment thread
kraftbj marked this conversation as resolved.
'default' => '',
Comment thread
kraftbj marked this conversation as resolved.
),
'format' => array(
'type' => 'string',
Expand Down Expand Up @@ -342,6 +364,34 @@ function press_this_rest_save_post( $request ) {
}
}

// Handle future (scheduled) status.
// We intentionally don't reject past dates here. WordPress core's
// wp_insert_post() auto-converts future+past-date to 'publish', so
// a past date simply publishes immediately — matching core REST API behavior.
if ( 'future' === $status ) {
if ( current_user_can( 'publish_posts' ) ) {
$date = $request->get_param( 'date' );
if ( empty( $date ) || false === strtotime( $date ) ) {
return new WP_Error(
'press_this_invalid_date',
__( 'A valid date is required to schedule a post.', 'press-this' ),
array( 'status' => 400 )
);
}
// The frontend sends a naive datetime in the site's local timezone (no TZ qualifier).
// WordPress sets PHP's timezone to UTC, so strtotime() interprets the string as UTC
// and gmdate() formats it back as UTC -- the round-trip preserves the original value.
// The result is the site-local time string we need for post_date.
$post_data['post_date'] = gmdate( 'Y-m-d H:i:s', strtotime( $date ) );
$post_data['post_date_gmt'] = get_gmt_from_date( $post_data['post_date'] );
$post_data['post_status'] = 'future';
// Required: wp_update_post ignores post_date changes unless edit_date is true.
$post_data['edit_date'] = true;
} else {
$post_data['post_status'] = 'pending';
}
}

// Side-load images from content.
// Require admin includes needed by media_sideload_image() (not loaded in REST context).
require_once ABSPATH . 'wp-admin/includes/file.php';
Expand Down
27 changes: 27 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export default function App() {
publishLabel: __( 'Publish', 'press-this' ),
} );

// Post status and date as React state so they update after scheduling.
const [ postStatus, setPostStatus ] = useState(
() => data.postStatus || ''
);
const [ postDate, setPostDate ] = useState( () => data.postDate || '' );

// Build initial post object for editor.
const post = useMemo(
() => ( {
Expand Down Expand Up @@ -316,6 +322,21 @@ export default function App() {
setSaveState( state );
}, [] );

/**
* Handle post status changes from PressThisEditor after a successful save.
* Updates local state so the Header reflects the new status (e.g., "Reschedule").
*
* @param {Object} change Status change details.
* @param {string} change.status New post status.
* @param {string} change.date New post date (ISO 8601).
*/
const handlePostStatusChange = useCallback( ( change ) => {
setPostStatus( change.status );
if ( change.date ) {
setPostDate( change.date );
}
}, [] );

// State for undo/redo from editor.
// Split into primitive values so React can skip re-renders when values
// haven't changed (Object.is comparison). The handlers are stable refs
Expand Down Expand Up @@ -360,6 +381,10 @@ export default function App() {
onRedo={ redoHandler }
hasUndo={ hasUndo }
hasRedo={ hasRedo }
capabilities={ capabilities }
timezone={ data.timezone }
postStatus={ postStatus }
postDate={ postDate }
/>

<div className="press-this-app__body">
Expand All @@ -377,6 +402,8 @@ export default function App() {
onScrapeProcessed={ handleScrapeProcessed }
onSaveReady={ handleSaveReady }
onUndoReady={ handleUndoReady }
timezone={ data.timezone }
onPostStatusChange={ handlePostStatusChange }
categoryNonce={ data.categoryNonce || '' }
ajaxUrl={ data.ajaxUrl || '' }
/>
Expand Down
Loading
Loading