state === 'active', 'active_required', 403); demand(content_execution_user_allowed($fresh->policy->execution_user_id), 'execution_user_denied', 403); $caps = effective_policy($fresh)->capabilities; $grants = ['media.upload' => 'media.upload', 'publication.create_draft' => 'draft.create', 'publication.amend_draft' => 'draft.update', 'publication.amend_published' => 'published.update', 'publication.create_publish' => 'publication.publish', 'publication.publish_draft' => 'publication.publish']; demand(isset($grants[$body->operation]), 'grant_denied', 403); $grant = $grants[$body->operation]; demand(in_array($grant, $caps, true), 'grant_denied', 403); if (in_array($body->operation, ['publication.create_draft', 'publication.create_publish'], true)) demand(can_create_posts($fresh->policy->execution_user_id), 'create_denied', 403); $a = $body->authorization; $now = microtime(true); $start = utc_seconds($a->approved_at); $end = utc_seconds($a->expires_at); demand($start <= $now && $now <= $end && $end - $start <= 900, 'approval_expired', 403); foreach (effect_hashes($body) as $key => $hash) if ($key !== 'execution_fingerprint') demand(hash_equals($body->{$key}, $hash), 'semantic_mismatch', 409); return $fresh; } function owned_handle($b, $kind, $handle) { $record = journal_read(journal_scope($b), $kind . ':' . $handle); demand($record !== null && isset($record->object_id) && $record->installation_id === $b->installation_id, 'handle_denied', 403); clean_post_cache($record->object_id); $post = get_post($record->object_id); demand($post && get_post_meta($post->ID, '_publisher_connector_scope', true) === journal_scope($b) && get_post_meta($post->ID, '_publisher_connector_handle', true) === $handle, 'handle_denied', 403); demand($post->post_type === ($kind === 'media' ? 'attachment' : 'post'), 'handle_denied', 403); return $record; } function validate_html($html, $binding = null) { $html = preg_replace_callback('~\[embed\](https://www\.youtube\.com/watch\?v=[A-Za-z0-9_-]{11})\[/embed\]~', static function ($match) { return ''; }, $html); $html = preg_replace_callback('~\[gallery ids="([1-9][0-9]*)" columns="1" size="full" link="none"\]~', static function ($match) use ($binding) { demand($binding !== null, 'gallery_binding_required', 403); $attachment_id = (int) $match[1]; $attachment = get_post($attachment_id); demand($attachment && $attachment->post_type === 'attachment' && wp_attachment_is_image($attachment_id) && in_array((int) $attachment->post_author, effective_policy($binding)->allowed_author_ids, true) && user_can($binding->policy->execution_user_id, 'read_post', $attachment_id), 'gallery_attachment_denied', 403); return ''; }, $html); demand(!preg_match('/<\?| [], 'br' => [], 'strong' => [], 'em' => [], 'ul' => [], 'ol' => [], 'li' => [], 'blockquote' => [], 'h2' => [], 'h3' => [], 'a' => ['href' => true, 'title' => true]]; demand(wp_kses($html, $allowed, ['https']) === $html, 'html_rejected'); demand(class_exists('DOMDocument'), 'html_parser_unavailable', 503); $dom = new \DOMDocument(); $prior = libxml_use_internal_errors(true); try { demand($dom->loadHTML('' . $html . '', LIBXML_NONET), 'html_rejected'); foreach ($dom->getElementsByTagName('*') as $element) { $tag = strtolower($element->tagName); if ($tag === 'html' || $tag === 'body') continue; demand(isset($allowed[$tag]), 'html_rejected'); foreach ($element->attributes as $attribute) demand(isset($allowed[$tag][$attribute->name]), 'html_rejected'); if ($tag === 'a' && $element->hasAttribute('href')) { $url = $element->getAttribute('href'); $parts = wp_parse_url($url); demand($parts && ($parts['scheme'] ?? '') === 'https' && isset($parts['host']) && !isset($parts['user']) && !isset($parts['pass']) && !preg_match('/[\x00-\x20\\\\]/', $url), 'html_rejected'); } } } finally { libxml_clear_errors(); libxml_use_internal_errors($prior); } } function validate_projection($b, $p) { $policy = effective_policy($b); $categories = $p->category_ids; sort($categories, SORT_NUMERIC); demand($categories === $p->category_ids && in_array($p->author_id, $policy->allowed_author_ids, true), 'target_denied', 403); foreach ($categories as $id) demand(in_array($id, $policy->allowed_category_ids, true) && category_exists($id) && user_can($b->policy->execution_user_id, 'assign_term', $id), 'target_denied', 403); demand(wp_strip_all_tags($p->title) === $p->title && wp_strip_all_tags($p->excerpt) === $p->excerpt && !str_contains($p->title . $p->excerpt, "\0"), 'text_rejected'); validate_html($p->body_html, $b); if ($p->media_handle !== null) owned_handle($b, 'media', $p->media_handle); } function projection_read($b, $record) { clean_post_cache($record->object_id); $post = get_post($record->object_id); demand($post && $post->post_type === 'post', 'object_absent', 409); $categories = wp_get_post_categories($post->ID, ['fields' => 'ids']); demand(!is_wp_error($categories), 'read_failed', 503); $categories = array_map('intval', $categories); sort($categories, SORT_NUMERIC); $media = $record->projection->media_handle; $attachment = $media === null ? 0 : (str_starts_with($media, 'adopted:') ? (int) substr($media, 8) : owned_handle($b, 'media', $media)->object_id); demand((int) get_post_thumbnail_id($post->ID) === (int) $attachment, 'content_drift', 409); return obj(['title' => $post->post_title, 'slug' => $post->post_name, 'excerpt' => $post->post_excerpt, 'body_html' => $post->post_content, 'author_id' => (int) $post->post_author, 'category_ids' => $categories, 'media_handle' => $media]); } function read_publication_by_url($b, $token) { demand(is_string($token) && preg_match('/^[A-Za-z0-9_-]{1,2731}$/D', $token), 'invalid_url', 400); $encoded = strtr($token, '-_', '+/'); $encoded .= str_repeat('=', (4 - strlen($encoded) % 4) % 4); $url = base64_decode($encoded, true); demand(is_string($url) && strlen($url) <= 2048 && rtrim(strtr(base64_encode($url), '+/', '-_'), '=') === $token, 'invalid_url', 400); $url = preg_replace('/#.*$/s', '', $url); $target = wp_parse_url($url); $homeUrl = home_url('/'); $home = wp_parse_url($homeUrl); demand($target && $home && ($target['scheme'] ?? '') === 'https' && strtolower($target['host'] ?? '') === strtolower($home['host'] ?? '') && ($target['port'] ?? null) === ($home['port'] ?? null) && !isset($target['user']) && !isset($target['pass']) && !preg_match('/[\x00-\x20\\\\]/', $url), 'site_mismatch', 403); $root = rtrim($home['path'] ?? '/', '/') . '/'; $path = $target['path'] ?? '/'; demand($root === '/' || str_starts_with($path, $root), 'site_mismatch', 403); $postId = url_to_postid($url); $post = $postId > 0 ? get_post($postId) : null; demand($post && $post->post_type === 'post' && get_post_status($post->ID) !== 'trash', 'not_found', 404); demand(user_can($b->policy->execution_user_id, 'read_post', $post->ID), 'auth_required', 403); $canonicalUrl = get_permalink($post); demand(is_string($canonicalUrl) && $canonicalUrl !== '', 'read_failed', 503); $canonical = wp_parse_url($canonicalUrl); demand($canonical && ($canonical['scheme'] ?? '') === 'https' && strtolower($canonical['host'] ?? '') === strtolower($home['host'] ?? '') && ($canonical['port'] ?? null) === ($home['port'] ?? null) && !isset($canonical['user']) && !isset($canonical['pass']) && !isset($canonical['fragment']) && ($root === '/' || str_starts_with($canonical['path'] ?? '/', $root)), 'read_failed', 503); $categories = wp_get_post_categories($post->ID, ['fields' => 'ids']); demand(!is_wp_error($categories), 'read_failed', 503); $categories = array_map('intval', $categories); sort($categories, SORT_NUMERIC); $featured = (int) get_post_thumbnail_id($post->ID); demand(strlen((string) $post->post_title) <= 1000 && strlen((string) $post->post_content) <= 100000, 'content_too_large', 413); $projection = obj(['title' => (string) $post->post_title, 'slug' => (string) $post->post_name, 'excerpt' => (string) $post->post_excerpt, 'body_html' => (string) $post->post_content, 'author_id' => (int) $post->post_author, 'category_ids' => $categories, 'media_handle' => null]); return obj(['protocol_version' => 1, 'tenant_id' => $b->tenant_id, 'site_id' => $b->site_id, 'installation_id' => $b->installation_id, 'connection_id' => $b->connection_id, 'policy_hash' => $b->policy_hash, 'canonical_url' => $canonicalUrl, 'post_id' => (int) $post->ID, 'status' => (string) get_post_status($post->ID), 'title' => (string) $post->post_title, 'content' => (string) $post->post_content, 'category_ids' => $categories, 'featured_media_id' => $featured > 0 ? $featured : null, 'projection' => $projection, 'content_hash' => semantic_hash($projection)]); } function locate_published_amendment($b, $publication) { $url = $publication->canonical_url; $target = wp_parse_url($url); $home = wp_parse_url(home_url('/')); demand($target && $home && ($target['scheme'] ?? '') === 'https' && strtolower($target['host'] ?? '') === strtolower($home['host'] ?? '') && ($target['port'] ?? null) === ($home['port'] ?? null) && !isset($target['user']) && !isset($target['pass']) && !isset($target['fragment']) && !preg_match('/[\x00-\x20\\\\]/', $url), 'site_mismatch', 403); $id = url_to_postid($url); $post = $id > 0 ? get_post($id) : null; demand($post && $post->post_type === 'post' && get_post_status($post->ID) === 'publish' && (int) $post->ID === $publication->post_id && hash_equals(rtrim(get_permalink($post), '/'), rtrim($url, '/')) && user_can($b->policy->execution_user_id, 'edit_post', $post->ID), 'published_changed', 409); demand(in_array((int) $post->post_author, effective_policy($b)->allowed_author_ids, true), 'target_denied', 403); $categories = wp_get_post_categories($post->ID, ['fields' => 'ids']); demand(!is_wp_error($categories), 'read_failed', 503); $categories = array_map('intval', $categories); sort($categories, SORT_NUMERIC); foreach ($categories as $category) demand(in_array($category, effective_policy($b)->allowed_category_ids, true), 'target_denied', 403); $media_id = (int) get_post_thumbnail_id($post->ID); $before = obj(['title' => $post->post_title, 'slug' => $post->post_name, 'excerpt' => $post->post_excerpt, 'body_html' => $post->post_content, 'author_id' => (int) $post->post_author, 'category_ids' => $categories, 'media_handle' => null]); $current_hash = semantic_hash($before); $record = obj(['installation_id' => $b->installation_id, 'object_id' => (int) $post->ID, 'projection' => obj(['title' => $post->post_title, 'slug' => $post->post_name, 'excerpt' => $post->post_excerpt, 'body_html' => $post->post_content, 'author_id' => (int) $post->post_author, 'category_ids' => $categories, 'media_handle' => null]), 'adopted_media_id' => $media_id]); demand($current_hash === $publication->expected_draft_hash, 'published_changed', 409); return $record; } function validate_publication($b, $publication) { validate_projection($b, $publication->projection); if ($publication->mode === 'amend_published') return locate_published_amendment($b, $publication); if (in_array($publication->mode, ['publish_draft', 'amend_draft'], true)) { $record = owned_handle($b, 'publication', $publication->draft_handle); demand(get_post_status($record->object_id) === 'draft' && user_can($b->policy->execution_user_id, 'edit_post', $record->object_id), 'draft_changed', 409); $before = projection_read($b, $record); $hash = semantic_hash($before); demand($hash === $publication->expected_draft_hash, 'draft_changed', 409); if ($publication->mode === 'publish_draft') demand($hash === semantic_hash($record->projection) && user_can($b->policy->execution_user_id, 'publish_posts') && $hash === semantic_hash($publication->projection), 'draft_changed', 409); if ($publication->mode === 'amend_draft') { $after = $publication->projection; demand($before->title === $after->title && $before->slug === $after->slug && $before->excerpt === $after->excerpt && $before->author_id === $after->author_id && canonical($before->category_ids) === canonical($after->category_ids), 'amendment_scope_denied', 403); } return $record; } return null; } function validate_media($media) { demand(!str_contains($media->filename, '..') && preg_match('/\.(png|jpe?g)$/iD', $media->filename) && !preg_match('/\.(php[0-9]?|phtml|phar|cgi|pl|html?)(\.|$)/i', $media->filename), 'media_name_rejected', 415); $bytes = base64_decode($media->bytes_base64, true); demand(is_string($bytes) && base64_encode($bytes) === $media->bytes_base64 && strlen($bytes) <= 8388608, 'media_limit', 413); demand(hash_equals($media->sha256, hash('sha256', $bytes)), 'media_hash_mismatch'); demand(!preg_match('/<\?(?:php|=)|mime_type && $info[0] <= 8192 && $info[1] <= 8192 && $info[0] * $info[1] <= 24000000, 'media_rejected', 415); if ($media->mime_type === 'image/png') { demand(substr($bytes, 0, 8) === "\x89PNG\r\n\x1a\n", 'media_rejected', 415); $offset = 8; $ended = false; while ($offset + 12 <= strlen($bytes)) { $len = unpack('N', substr($bytes, $offset, 4))[1]; demand($len <= strlen($bytes) - $offset - 12, 'media_rejected', 415); $chunk = substr($bytes, $offset + 4, $len + 4); $type = substr($chunk, 0, 4); demand(hash_equals(hash('crc32b', $chunk, true), substr($bytes, $offset + 8 + $len, 4)), 'media_rejected', 415); $offset += $len + 12; if ($type === 'IEND') { demand($len === 0 && $offset === strlen($bytes), 'media_rejected', 415); $ended = true; break; } } demand($ended, 'media_rejected', 415); } else demand(substr($bytes, 0, 2) === "\xff\xd8" && substr($bytes, -2) === "\xff\xd9", 'media_rejected', 415); demand(function_exists('imagecreatefromstring'), 'image_decoder_unavailable', 503); $image = @imagecreatefromstring($bytes); demand($image !== false, 'media_rejected', 415); imagedestroy($image); return $bytes; } function operation_status($b, $key, $row) { $v = pick($b, ['protocol_version', 'tenant_id', 'site_id', 'installation_id']); $v->idempotency_key = $key; $v->state = $row->state === 'completed' ? 'completed' : 'unresolved'; $v->receipt = $row->state === 'completed' ? $row->receipt : null; $v->observed = $v->receipt === null ? null : observe_receipt($b, $v->receipt); return schema('operationStatus', $v); } function observe_receipt($b, $receipt) { $out = obj(['state' => 'unknown', 'observed_at' => utc_now(), 'content_hash' => null, 'reason' => 'read_failed']); try { $r = $receipt->result; $id = $r->kind === 'media' ? $r->attachment_id : $r->post_id; clean_post_cache($id); if (!get_post($id)) { $out->state = 'absent'; $out->reason = 'object_absent'; return $out; } // Published corrections target the existing post directly; they do not create a draft handle. if ($r->kind === 'publication' && $receipt->operation === 'publication.amend_published' && $r->publication_handle === null) { $post = get_post($id); demand($post->post_type === 'post', 'content_drift', 409); $categories = wp_get_post_categories($id, ['fields' => 'ids']); demand(!is_wp_error($categories), 'read_failed', 503); $categories = array_map('intval', $categories); sort($categories, SORT_NUMERIC); $media_id = $r->projection->media_handle === null ? 0 : owned_handle($b, 'media', $r->projection->media_handle)->object_id; if ($r->projection->media_handle === null && isset($receipt->idempotency_key)) { $operation = journal_read(journal_scope($b), 'operation:' . $receipt->idempotency_key); if ($operation && isset($operation->preserved_featured_media_id)) { demand($operation->state === 'completed' && isset($operation->receipt->receipt_hash, $receipt->receipt_hash) && hash_equals($operation->receipt->receipt_hash, $receipt->receipt_hash), 'content_drift', 409); demand(is_int($operation->preserved_featured_media_id) && $operation->preserved_featured_media_id >= 0, 'content_drift', 409); $media_id = $operation->preserved_featured_media_id; } } $projection = published_amendment_projection($post, $categories, $r->projection->media_handle, $media_id, (int) get_post_thumbnail_id($id)); demand(get_permalink($id) === $r->public_url, 'content_drift', 409); $hash = semantic_hash($projection); $expected = $r->content_hash; } else { $record = owned_handle($b, $r->kind, $r->kind === 'media' ? $r->media_handle : $r->publication_handle); if ($r->kind === 'media') { $path = get_attached_file($id); demand($path && is_file($path), 'read_failed', 503); $hash = hash_file('sha256', $path); demand(is_string($hash), 'read_failed', 503); $expected = $r->original_sha256; } else { $hash = semantic_hash(projection_read($b, $record)); $expected = $r->content_hash; } } $out->content_hash = $hash; $out->state = $hash === $expected ? 'match' : 'mismatch'; $out->reason = $hash === $expected ? 'none' : 'content_drift'; if ($r->kind === 'publication' && get_post_status($id) !== $r->status) { $out->state = 'mismatch'; $out->reason = 'status_drift'; } } catch (Failure $e) { if (in_array($e->getMessage(), ['content_drift', 'publication_changed'], true)) { $out->state = 'mismatch'; $out->reason = 'content_drift'; } elseif ($e->getMessage() === 'handle_denied') $out->reason = 'identity_ambiguous'; } return $out; } function execute_effect($b, $body) { try { // Serialise content writes with local disconnect/setup proof. Once a // revoked proof is observed no old in-flight writer can finish later. return with_setup_lock(fn() => with_effect_lock(journal_scope($b), $body->idempotency_key, fn() => execute_effect_locked($b, $body), 5)); } catch (Failure $e) { if ($e->getMessage() !== 'operation_busy') throw $e; require_grant($b, $body); $row = journal_read(journal_scope($b), 'operation:' . $body->idempotency_key); if (!$row) throw $e; // No durable reservation is not permission to retry. demand($row->execution_fingerprint === effect_hashes($body)['execution_fingerprint'], 'idempotency_conflict', 409); return $row->state === 'completed' ? [$row->receipt, in_array($body->operation, ['publication.publish_draft', 'publication.amend_draft'], true) ? 200 : 201] : [operation_status($b, $body->idempotency_key, $row), 202]; } } function execute_effect_locked($b, $body) { require_grant($b, $body); $scope = journal_scope($b); $slot = 'operation:' . $body->idempotency_key; $fingerprint = effect_hashes($body)['execution_fingerprint']; $existing = journal_read($scope, $slot); if ($existing) { demand($existing->execution_fingerprint === $fingerprint, 'idempotency_conflict', 409); demand($existing->state!=='cancelled','operation_retired',409); if ($existing->state === 'completed') return [$existing->receipt, in_array($body->operation, ['publication.publish_draft', 'publication.amend_draft'], true) ? 200 : 201]; return [operation_status($b, $body->idempotency_key, $existing), 202]; } $bytes = null; if ($body->operation === 'media.upload') $bytes = validate_media($body->media); else validate_publication($b, $body->publication); $row = obj(['state' => 'reserved', 'execution_fingerprint' => $fingerprint, 'request_audit_fingerprint' => semantic_hash(obj(['key_id' => $b->key_id, 'body' => $body])), 'receipt' => null]); if (!journal_insert($scope, $slot, $row)) { $existing = journal_read($scope, $slot); demand($existing->execution_fingerprint === $fingerprint, 'idempotency_conflict', 409); return $existing->state === 'completed' ? [$existing->receipt, in_array($body->operation, ['publication.publish_draft', 'publication.amend_draft'], true) ? 200 : 201] : [operation_status($b, $body->idempotency_key, $existing), 202]; } $executing = clone $row; $executing->state = 'executing'; journal_cas($scope, $slot, $row, $executing); $row = $executing; $previous_user = get_current_user_id(); $execution_user = $b->policy->execution_user_id; $content_caps = array_fill_keys(content_execution_capabilities(), true); $restrict_caps = static function ($allcaps, $required, $args, $user) use ($execution_user, $content_caps) { return $user->ID === $execution_user ? array_intersect_key($allcaps, $content_caps) : $allcaps; }; try { // Recheck after consuming the operation, before adopting any user identity. require_grant($b, $body); add_filter('user_has_cap', $restrict_caps, PHP_INT_MAX, 4); wp_set_current_user($b->policy->execution_user_id); require_grant($b, $body); if ($body->operation === 'media.upload') { $result = create_media($b, $body, $bytes, $scope, $slot, $row); } else { $record = validate_publication($b, $body->publication); $result = create_publication($b, $body, $record, $scope, $slot, $row); } $receipt = pick($body, ['protocol_version', 'tenant_id', 'site_id', 'installation_id', 'effect_id', 'operation', 'idempotency_key', 'subject_hash', 'target_fingerprint', 'policy_hash']); $receipt->original_connection_id = $b->connection_id; $receipt->state = 'completed'; $receipt->write_attempted = true; $receipt->result = $result; $receipt->receipt_hash = semantic_hash($receipt); schema('receipt', $receipt); $complete = clone $row; $complete->state = 'completed'; $complete->receipt = $receipt; journal_cas($scope, $slot, $row, $complete); return [$receipt, in_array($body->operation, ['publication.publish_draft', 'publication.amend_draft'], true) ? 200 : 201]; } catch (\Throwable $e) { // Once executing is durable, any exception is unresolved, including WP hooks // or the receipt store failing. Never expose a false no-write assertion. $row->state = 'unresolved'; return [operation_status($b, $body->idempotency_key, $row), 202]; } finally { wp_set_current_user($previous_user); remove_filter('user_has_cap', $restrict_caps, PHP_INT_MAX); } } function correlate($scope, $slot, &$row, $id) { $next = clone $row; $next->object_id = $id; journal_cas($scope, $slot, $row, $next); $row = $next; } function update_published_amendment($post_id, $body_html, $category_ids, $media_id, $adopted_media_id) { $fields = ['ID' => $post_id, 'post_status' => 'publish', 'post_content' => $body_html, 'post_category' => $category_ids]; if ($media_id !== (int) $adopted_media_id) $fields['meta_input'] = ['_thumbnail_id' => $media_id]; return wp_update_post(wp_slash($fields), true); } function published_amendment_projection($post, $categories, $media_handle, $expected_media_id, $observed_media_id) { demand((int) $observed_media_id === (int) $expected_media_id, 'publication_changed', 503); return obj(['title' => $post->post_title, 'slug' => $post->post_name, 'excerpt' => $post->post_excerpt, 'body_html' => $post->post_content, 'author_id' => (int) $post->post_author, 'category_ids' => $categories, 'media_handle' => $media_handle]); } function create_media($b, $body, $bytes, $scope, $slot, &$row) { require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $handle = wp_generate_uuid4(); $name = 'publisher-' . $handle . ($body->media->mime_type === 'image/png' ? '.png' : '.jpg'); require_grant($b, $body); $upload = wp_upload_bits($name, null, $bytes); demand(empty($upload['error']), 'upload_uncertain', 503); require_grant($b, $body); $id = wp_insert_attachment(wp_slash(['post_mime_type' => $body->media->mime_type, 'post_title' => 'Publisher image', 'post_status' => 'inherit', 'post_author' => $b->policy->execution_user_id, 'meta_input' => ['_publisher_connector_scope' => $scope, '_publisher_connector_handle' => $handle, '_publisher_connector_original_sha256' => $body->media->sha256, '_wp_attachment_image_alt' => $body->media->alt_text, '_publisher_connector_credit' => $body->media->credit]]), $upload['file'], 0, true); demand(!is_wp_error($id) && $id > 0, 'upload_uncertain', 503); correlate($scope, $slot, $row, (int) $id); $record = obj(['installation_id' => $b->installation_id, 'object_id' => (int) $id, 'original_sha256' => $body->media->sha256]); demand(journal_insert($scope, 'media:' . $handle, $record), 'handle_conflict', 503); $no_scale = function () { return false; }; add_filter('big_image_size_threshold', $no_scale, PHP_INT_MAX); try { $metadata = wp_generate_attachment_metadata($id, $upload['file']); } finally { remove_filter('big_image_size_threshold', $no_scale, PHP_INT_MAX); } demand(is_array($metadata), 'upload_uncertain', 503); wp_update_attachment_metadata($id, $metadata); owned_handle($b, 'media', $handle); demand(hash_file('sha256', get_attached_file($id)) === $body->media->sha256, 'upload_changed', 503); return obj(['kind' => 'media', 'media_handle' => $handle, 'attachment_id' => (int) $id, 'original_sha256' => $body->media->sha256]); } function create_publication($b, $body, $record, $scope, $slot, &$row) { $p = $body->publication->projection; $status = in_array($body->operation, ['publication.create_draft', 'publication.amend_draft'], true) ? 'draft' : 'publish'; $handle = $record ? $body->publication->draft_handle : wp_generate_uuid4(); require_grant($b, $body); validate_publication($b, $body->publication); if ($record && $body->operation === 'publication.amend_published') { demand((int) $record->object_id === $body->publication->post_id, 'published_changed', 409); $media_id = $p->media_handle === null ? (int) $record->adopted_media_id : owned_handle($b, 'media', $p->media_handle)->object_id; if ($p->media_handle === null) { demand((int) get_post_thumbnail_id($record->object_id) === $media_id, 'published_changed', 409); // Persist the existing photo before the write; readback must detect later photo drift. $retained = clone $row; $retained->preserved_featured_media_id = $media_id; journal_cas($scope, $slot, $row, $retained); $row = $retained; } $id = update_published_amendment($record->object_id, $p->body_html, $p->category_ids, $media_id, $record->adopted_media_id); } elseif ($record && $body->operation === 'publication.publish_draft') $id = wp_update_post(['ID' => $record->object_id, 'post_status' => 'publish'], true); elseif ($record && $body->operation === 'publication.amend_draft') { $media_id = $p->media_handle === null ? 0 : owned_handle($b, 'media', $p->media_handle)->object_id; $id = wp_update_post(wp_slash(['ID' => $record->object_id, 'post_status' => 'draft', 'post_content' => $p->body_html, 'meta_input' => ['_thumbnail_id' => $media_id]]), true); } else { $media_id = $p->media_handle === null ? 0 : owned_handle($b, 'media', $p->media_handle)->object_id; $fields = ['post_type' => 'post', 'post_status' => $status, 'post_title' => $p->title, 'post_name' => $p->slug, 'post_excerpt' => $p->excerpt, 'post_content' => $p->body_html, 'post_author' => $p->author_id, 'post_category' => $p->category_ids, 'meta_input' => ['_publisher_connector_scope' => $scope, '_publisher_connector_handle' => $handle]]; if ($media_id) $fields['meta_input']['_thumbnail_id'] = $media_id; $id = wp_insert_post(wp_slash($fields), true); } demand(!is_wp_error($id) && $id > 0, 'publication_uncertain', 503); correlate($scope, $slot, $row, (int) $id); if (!$record) { $record = obj(['installation_id' => $b->installation_id, 'object_id' => (int) $id, 'projection' => $p]); demand(journal_insert($scope, 'publication:' . $handle, $record), 'handle_conflict', 503); } elseif ($body->operation === 'publication.amend_draft') { $updated = clone $record; $updated->projection = $p; journal_cas($scope, 'publication:' . $handle, $record, $updated); $record = $updated; } if ($body->operation === 'publication.amend_published') { demand((int) $id === $body->publication->post_id, 'published_changed', 409); clean_post_cache($id); $post = get_post($id); $categories = wp_get_post_categories($id, ['fields' => 'ids']); demand($post && !is_wp_error($categories), 'publication_changed', 503); $categories = array_map('intval', $categories); sort($categories, SORT_NUMERIC); $observed = published_amendment_projection($post, $categories, $p->media_handle, $media_id, (int) get_post_thumbnail_id($id)); } else { owned_handle($b, 'publication', $handle); $observed = projection_read($b, $record); } demand(canonical($observed) === canonical($p) && get_post_status($id) === $status, 'publication_changed', 503); $url = $status === 'publish' ? get_permalink($id) : null; if ($url !== null) { $home = trailingslashit(home_url('/')); demand(str_starts_with($home, 'https://') && str_starts_with($url, $home) && !preg_match('/[\x00-\x20]/', $url), 'public_url_rejected', 503); } return obj(['kind' => 'publication', 'publication_handle' => $body->operation === 'publication.amend_published' ? null : $handle, 'post_id' => (int) $id, 'status' => $status, 'projection' => $observed, 'content_hash' => semantic_hash($observed), 'public_url' => $url]); }