Bump plugin version so the us_invites schema migration actually runs
CI / Tests (PHP 8.2) (pull_request) Successful in 37s
CI / Tests (PHP 8.1) (pull_request) Successful in 44s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 2m55s
CI / PHPStan (pull_request) Successful in 2m54s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m36s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.2) (pull_request) Successful in 37s
CI / Tests (PHP 8.1) (pull_request) Successful in 44s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 2m55s
CI / PHPStan (pull_request) Successful in 2m54s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m36s
CI / Build Plugin Zip (pull_request) Skipped
PR #83 added kind and expires_at to us_invites and the repository started writing them, but USC_VERSION stayed at 1.0.0-rc.2 — Plugin::boot() only re-runs Installer/dbDelta on a version mismatch, so upgraded sites never got the columns. Every invite insert then failed silently: nothing appeared under Pending Invites while the admin was still shown a registration link whose token hash was never stored. - Version / USC_VERSION -> 1.0.0-rc.3 (triggers dbDelta on next load). - InviteRepository::insert() returns 0 on failure instead of a stale insert_id, and the Invites page now shows an error notice instead of a dead link when creation fails (personal and group forms), including clearer validation messages. - CLAUDE.md: schema changes must bump the version. Closes #87 Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -11,8 +11,12 @@ class InviteRepository {
|
||||
$this->table = $db->prefix . 'us_invites';
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist an invite. Returns the new row id, or 0 when the insert failed —
|
||||
* callers must not hand out a registration link for an unstored token.
|
||||
*/
|
||||
public function insert( Invite $invite ): int {
|
||||
$this->db->insert(
|
||||
$result = $this->db->insert(
|
||||
$this->table,
|
||||
[
|
||||
'email' => $invite->email,
|
||||
@@ -29,7 +33,7 @@ class InviteRepository {
|
||||
[ '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s', '%s' ]
|
||||
);
|
||||
|
||||
return $this->db->insert_id;
|
||||
return false === $result ? 0 : $this->db->insert_id;
|
||||
}
|
||||
|
||||
public function findByToken( string $token ): ?Invite {
|
||||
|
||||
@@ -20,8 +20,9 @@ class RegistrationController {
|
||||
}
|
||||
|
||||
$newInviteUrl = '';
|
||||
$inviteError = '';
|
||||
if ( isset( $_POST['usc_action'] ) && check_admin_referer( 'usc_invite_action' ) ) {
|
||||
$newInviteUrl = $this->handleFormAction();
|
||||
[ $newInviteUrl, $inviteError ] = $this->handleFormAction();
|
||||
}
|
||||
|
||||
$pendingInvites = $this->invites->findPending();
|
||||
@@ -32,11 +33,14 @@ class RegistrationController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a posted admin action. Returns the registration link for a freshly
|
||||
* created invite — the only time it can be shown, since just the token's hash
|
||||
* is stored — or an empty string for every other action.
|
||||
* Handle a posted admin action. Returns `[link, error]`: the registration
|
||||
* link for a freshly created invite — the only time it can be shown, since
|
||||
* just the token's hash is stored — or an error message when creation
|
||||
* failed; both empty for every other action.
|
||||
*
|
||||
* @return array{string, string}
|
||||
*/
|
||||
private function handleFormAction(): string {
|
||||
private function handleFormAction(): array {
|
||||
// Nonce is verified by the caller (renderPage) before this method runs.
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
$action = sanitize_key( Val::string( wp_unslash( $_POST['usc_action'] ?? '' ) ) );
|
||||
@@ -49,42 +53,46 @@ class RegistrationController {
|
||||
$email = sanitize_email( Val::string( wp_unslash( $_POST['email'] ?? '' ) ) );
|
||||
|
||||
if (
|
||||
is_email( $email )
|
||||
&& false === email_exists( $email )
|
||||
&& null === $this->invites->findPendingByEmail( $email )
|
||||
! is_email( $email )
|
||||
|| false !== email_exists( $email )
|
||||
|| null !== $this->invites->findPendingByEmail( $email )
|
||||
) {
|
||||
$rawToken = wp_generate_password( 32, false );
|
||||
|
||||
$this->invites->insert(
|
||||
new Invite(
|
||||
email: $email,
|
||||
token: Invite::hashToken( $rawToken ),
|
||||
invitedBy: get_current_user_id(),
|
||||
)
|
||||
);
|
||||
|
||||
return $this->registrationLink( $rawToken );
|
||||
return [ '', esc_html__( 'Could not create the invite: enter a valid email address that has no account and no pending invite.', 'unsupervised-schedular' ) ];
|
||||
}
|
||||
|
||||
$rawToken = wp_generate_password( 32, false );
|
||||
|
||||
$id = $this->invites->insert(
|
||||
new Invite(
|
||||
email: $email,
|
||||
token: Invite::hashToken( $rawToken ),
|
||||
invitedBy: get_current_user_id(),
|
||||
)
|
||||
);
|
||||
|
||||
return $this->linkOrError( $id, $rawToken );
|
||||
}
|
||||
|
||||
if ( 'group_invite' === $action ) {
|
||||
$expiresAt = $this->normalizeExpiry( sanitize_text_field( Val::string( wp_unslash( $_POST['expires_at'] ?? '' ) ) ) );
|
||||
|
||||
if ( null !== $expiresAt ) {
|
||||
$rawToken = wp_generate_password( 32, false );
|
||||
|
||||
$this->invites->insert(
|
||||
new Invite(
|
||||
email: '',
|
||||
token: Invite::hashToken( $rawToken ),
|
||||
invitedBy: get_current_user_id(),
|
||||
kind: Invite::KIND_GROUP,
|
||||
expiresAt: $expiresAt,
|
||||
)
|
||||
);
|
||||
|
||||
return $this->registrationLink( $rawToken );
|
||||
if ( null === $expiresAt ) {
|
||||
return [ '', esc_html__( 'Could not create the group link: choose an expiry date of today or later.', 'unsupervised-schedular' ) ];
|
||||
}
|
||||
|
||||
$rawToken = wp_generate_password( 32, false );
|
||||
|
||||
$id = $this->invites->insert(
|
||||
new Invite(
|
||||
email: '',
|
||||
token: Invite::hashToken( $rawToken ),
|
||||
invitedBy: get_current_user_id(),
|
||||
kind: Invite::KIND_GROUP,
|
||||
expiresAt: $expiresAt,
|
||||
)
|
||||
);
|
||||
|
||||
return $this->linkOrError( $id, $rawToken );
|
||||
}
|
||||
|
||||
if ( 'revoke' === $action ) {
|
||||
@@ -95,7 +103,22 @@ class RegistrationController {
|
||||
}
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
|
||||
return '';
|
||||
return [ '', '' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* The registration link for a stored invite, or an error when the insert
|
||||
* failed — a link must never be shown for a token that was not persisted,
|
||||
* since it could only ever dead-end as "invalid or expired".
|
||||
*
|
||||
* @return array{string, string}
|
||||
*/
|
||||
private function linkOrError( int $insertedId, string $rawToken ): array {
|
||||
if ( $insertedId <= 0 ) {
|
||||
return [ '', esc_html__( 'Could not save the invite. Deactivate and reactivate the plugin to update the database, then try again.', 'unsupervised-schedular' ) ];
|
||||
}
|
||||
|
||||
return [ $this->registrationLink( $rawToken ), '' ];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user