post_title, $post->post_excerpt, $post->post_content); } /** * Read the post and decide. * * @since 0.1.4 * @param string|null $expectedHash The hash the plugin stored at its last write, or null when there is none (the * post was not written by the plugin, or the record predates hashes). * @return array{allowed: bool, reason: string, locked_by: int} */ public function check(int $postId, ?string $expectedHash): array { $post = get_post($postId); if (!$post instanceof \WP_Post) { return self::decide(['exists' => false], $expectedHash); } if (!function_exists('wp_check_post_lock')) { // Not loaded in a REST request; core's own REST controllers load it the same way. require_once ABSPATH . 'wp-admin/includes/post.php'; } $lock = wp_check_post_lock($post->ID); return self::decide([ 'exists' => true, 'status' => $post->post_status, 'can_edit' => current_user_can('edit_post', $post->ID), 'locked_by' => is_int($lock) ? $lock : 0, 'hash' => self::hashOf($post), ], $expectedHash); } /** * The rules. Order matters: the first failing rule is the reason given. * * @since 0.1.4 * @param array{exists: bool, status?: string, can_edit?: bool, locked_by?: int, hash?: string} $facts * @return array{allowed: bool, reason: string, locked_by: int} */ public static function decide(array $facts, ?string $expectedHash): array { $deny = static fn (string $reason, int $lockedBy = 0): array => ['allowed' => false, 'reason' => $reason, 'locked_by' => $lockedBy]; if (!($facts['exists'] ?? false)) { return $deny(self::MISSING); } $status = (string) ($facts['status'] ?? ''); if ($status === 'trash') { return $deny(self::TRASHED); } if (!($facts['can_edit'] ?? false)) { return $deny(self::CAPABILITY); } if (!in_array($status, ['draft', 'pending'], true)) { return $deny(self::STATUS); } if (($facts['locked_by'] ?? 0) > 0) { return $deny(self::LOCKED, (int) $facts['locked_by']); } if ($expectedHash !== null && $expectedHash !== '' && !hash_equals($expectedHash, (string) ($facts['hash'] ?? ''))) { return $deny(self::EDITED); } return ['allowed' => true, 'reason' => self::OK, 'locked_by' => 0]; } /** * A sentence for the person. Needs WordPress for translations and the lock holder's name. * * @since 0.1.4 * @param array{reason: string, locked_by: int} $verdict */ public static function message(array $verdict): string { switch ($verdict['reason']) { case self::MISSING: return __('That post no longer exists.', 'maze-graph'); case self::TRASHED: return __('That post is in the trash.', 'maze-graph'); case self::CAPABILITY: return __('You may not edit that post.', 'maze-graph'); case self::STATUS: return __('That post is no longer a draft, so MazeGraph will not change it. Change it in the editor.', 'maze-graph'); case self::LOCKED: $user = get_userdata($verdict['locked_by']); /* translators: %s: name of the person editing the post. */ return sprintf(__('%s is editing that post right now.', 'maze-graph'), $user instanceof \WP_User ? $user->display_name : __('Someone', 'maze-graph')); case self::EDITED: return __('That post was edited in WordPress after MazeGraph wrote it, so MazeGraph will not overwrite or remove it. Use the editor, or the post\'s Revisions screen.', 'maze-graph'); } return ''; } }