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
52 changes: 52 additions & 0 deletions scripts/cow/merge.php
Original file line number Diff line number Diff line change
Expand Up @@ -10325,6 +10325,7 @@ function cow_merge_wordpress_attachment_upload_issues(string $target_db, string
'field' => $candidate['field'] ?? null,
'role' => $candidate['role'] ?? null,
],
'resolution_policy' => 'review-only',
'manual_review_reason' => 'WordPress attachment metadata points at upload files that are not present in the merged filesystem.',
'suggested_action' => 'Restore the missing upload file, update the attachment metadata, or regenerate media derivatives in WordPress before accepting the merged state.',
'candidate' => [
Expand Down Expand Up @@ -10493,6 +10494,29 @@ function cow_merge_wordpress_attachment_upload_issues(string $target_db, string
continue;
}

$metadata_width = $metadata['width'] ?? null;
$metadata_height = $metadata['height'] ?? null;
if (!is_numeric($metadata_width) || !is_numeric($metadata_height) || (int)$metadata_width <= 0 || (int)$metadata_height <= 0) {
$record_issue($issues, $attachment_id, $post_title, 'plugin-wp-attachment-metadata-invalid-shape', 'attachment original dimensions are invalid', [
'field' => '_wp_attachment_metadata.dimensions',
'role' => 'metadata-dimensions',
'attached_file' => $attached_file_raw,
'width' => $metadata_width,
'height' => $metadata_height,
], [$attached_path]);
}
$attached_absolute_path = rtrim($target_root, "/\\") . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $attached_path);
$declared_filesize = $metadata['filesize'] ?? null;
if ($declared_filesize !== null && is_file($attached_absolute_path) && (!is_numeric($declared_filesize) || (int)$declared_filesize !== (int)filesize($attached_absolute_path))) {
$record_issue($issues, $attachment_id, $post_title, 'plugin-wp-attachment-upload-filesize-drift', 'attachment original filesize metadata does not match the upload file', [
'field' => '_wp_attachment_metadata.filesize',
'role' => 'metadata-filesize',
'attached_file' => $attached_file_raw,
'declared_filesize' => $declared_filesize,
'actual_filesize' => (int)filesize($attached_absolute_path),
], [$attached_path]);
}

$base_path = $attached_path;
if (isset($metadata['file']) && is_string($metadata['file']) && trim($metadata['file']) !== '') {
$metadata_path = cow_merge_wordpress_upload_relative_path((string)$metadata['file']);
Expand Down Expand Up @@ -10554,6 +10578,32 @@ function cow_merge_wordpress_attachment_upload_issues(string $target_db, string
'size' => (string)$size_name,
'generated_file' => (string)$size['file'],
]);
$size_width = $size['width'] ?? null;
$size_height = $size['height'] ?? null;
if (!is_numeric($size_width) || !is_numeric($size_height) || (int)$size_width <= 0 || (int)$size_height <= 0) {
$record_issue($issues, $attachment_id, $post_title, 'plugin-wp-attachment-metadata-invalid-shape', 'attachment generated-size dimensions are invalid', [
'field' => '_wp_attachment_metadata.sizes.' . (string)$size_name . '.dimensions',
'role' => 'generated-size-dimensions',
'size' => (string)$size_name,
'attached_file' => $attached_file_raw,
'generated_file' => (string)$size['file'],
'width' => $size_width,
'height' => $size_height,
], [$size_path]);
}
$size_absolute_path = rtrim($target_root, "/\\") . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $size_path);
$declared_size_filesize = $size['filesize'] ?? null;
if ($declared_size_filesize !== null && is_file($size_absolute_path) && (!is_numeric($declared_size_filesize) || (int)$declared_size_filesize !== (int)filesize($size_absolute_path))) {
$record_issue($issues, $attachment_id, $post_title, 'plugin-wp-attachment-upload-filesize-drift', 'attachment generated-size filesize metadata does not match the upload file', [
'field' => '_wp_attachment_metadata.sizes.' . (string)$size_name . '.filesize',
'role' => 'generated-size-filesize',
'size' => (string)$size_name,
'attached_file' => $attached_file_raw,
'generated_file' => (string)$size['file'],
'declared_filesize' => $declared_size_filesize,
'actual_filesize' => (int)filesize($size_absolute_path),
], [$size_path]);
}
}

if (isset($metadata['original_image']) && is_string($metadata['original_image']) && trim($metadata['original_image']) !== '') {
Expand Down Expand Up @@ -10636,6 +10686,7 @@ function cow_merge_wordpress_attachment_upload_issues(string $target_db, string
'kind' => 'wordpress-attachment-upload-ownership',
'file' => $path,
],
'resolution_policy' => 'review-only',
'manual_review_reason' => 'WordPress attachment metadata assigns one upload file to multiple attachments.',
'suggested_action' => 'Review the attachment rows and metadata, then keep one owner or create distinct upload files before accepting the merged state.',
'candidate' => [
Expand Down Expand Up @@ -10676,6 +10727,7 @@ function cow_merge_wordpress_attachment_upload_issues(string $target_db, string
'kind' => 'wordpress-attachment-upload-case-collision',
'case_insensitive_file' => $case_key,
],
'resolution_policy' => 'review-only',
'manual_review_reason' => 'Default macOS and Windows filesystems treat these upload paths as the same file, making attachment ownership ambiguous across platforms.',
'suggested_action' => 'Rename or regenerate one upload path so attachment metadata is distinct after case folding before accepting the merged state.',
'candidate' => [
Expand Down
24 changes: 22 additions & 2 deletions tests/cow/media_validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -1564,7 +1564,7 @@ function insert_attachment_with_single_meta(SQLite3 $db, string $title, string $

assert_same($result['status'], 'completed_with_conflicts', 'media validator holds incomplete generated-size metadata for review');
assert_same((int)($result['plugin_validators'] ?? 0), 1, 'media validator is discovered from mu-plugins during merge');
assert_same((int)($result['plugin_validator_conflicts'] ?? 0), 97, 'media validator records missing required metadata, invalid metadata, invalid shapes, dimensions, image metadata, filesize and MIME drift, invalid file entries, generated-size, original-image, backup-size, missing-file, metadata-file drift, unsafe path, duplicate upload conflicts, and built-in WordPress upload conflicts');
assert_same((int)($result['plugin_validator_conflicts'] ?? 0), 101, 'media validator records missing required metadata, invalid metadata, invalid shapes, dimensions, image metadata, filesize and MIME drift, invalid file entries, generated-size, original-image, backup-size, missing-file, metadata-file drift, unsafe path, duplicate upload conflicts, and built-in WordPress upload conflicts');
assert_same(
scalar($target, "SELECT meta_value FROM wp_postmeta WHERE post_id = $attachment_id AND meta_key = '_wp_attached_file'"),
'2026/05/source-generated-missing-file-key.jpg',
Expand Down Expand Up @@ -1650,7 +1650,7 @@ function insert_attachment_with_single_meta(SQLite3 $db, string $title, string $
'records' => 'conflicts',
'conflict_type' => 'plugin-wp-attachment-metadata-invalid-shape',
]);
assert_same(count($invalid_shape_audit['conflicts']), 4, 'media validator exposes malformed generated-size and backup-size metadata shapes as plugin-scoped audit conflicts');
assert_same(count($invalid_shape_audit['conflicts']), 6, 'media validator exposes malformed generated-size and backup-size metadata shapes as plugin-scoped audit conflicts');
$invalid_shape_payloads = array_map(
fn($conflict) => cow_merge_decode_payload_json((string)$conflict['chosen_payload'], 'media validator invalid-shape payload'),
$invalid_shape_audit['conflicts']
Expand All @@ -1660,7 +1660,9 @@ function insert_attachment_with_single_meta(SQLite3 $db, string $title, string $
assert_same($invalid_shape_fields, [
'_wp_attachment_metadata.backup_sizes',
'_wp_attachment_metadata.backup_sizes.full-orig.file',
'_wp_attachment_metadata.dimensions',
'_wp_attachment_metadata.sizes',
'_wp_attachment_metadata.sizes.thumbnail.dimensions',
'_wp_attachment_metadata.sizes.thumbnail.file',
], 'media validator invalid-shape audit identifies every malformed metadata field');
assert_same($invalid_shape_audit['conflicts'][0]['plugin_files'] ?? null, [], 'media validator invalid-shape audit does not expose guessed upload paths');
Expand Down Expand Up @@ -1714,6 +1716,24 @@ function insert_attachment_with_single_meta(SQLite3 $db, string $title, string $
$meta_db->close();
assert_true($generated_filesize_recorded, 'media validator filesize audit payload identifies the affected generated file');
assert_true(str_contains($filesize_preview, (string)$generated_filesize_drift_id), 'media validator filesize audit includes the generated filesize attachment ID');
$wp_upload_filesize_audit = cow_merge_audit_report($metadata, (int)$result['run_id'], 10, [
'scope' => 'plugin',
'records' => 'conflicts',
'semantic_scope' => 'wordpress',
'conflict_type' => 'plugin-wp-attachment-upload-filesize-drift',
]);
assert_same(count($wp_upload_filesize_audit['conflicts']), 2, 'built-in WordPress upload validator exposes original and generated filesize drift as review-only conflicts');
$wp_upload_filesize_payloads = array_map(
fn($conflict) => cow_merge_decode_payload_json((string)$conflict['chosen_payload'], 'media validator WordPress upload filesize payload'),
$wp_upload_filesize_audit['conflicts']
);
$wp_upload_filesize_fields = array_map(fn($payload) => (string)($payload['candidate']['field'] ?? ''), $wp_upload_filesize_payloads);
sort($wp_upload_filesize_fields, SORT_STRING);
assert_same($wp_upload_filesize_fields, [
'_wp_attachment_metadata.filesize',
'_wp_attachment_metadata.sizes.thumbnail.filesize',
], 'built-in WordPress upload validator names stale filesize metadata fields');
assert_same($wp_upload_filesize_audit['conflicts'][0]['plugin_resolution_policy'] ?? null, 'review-only', 'built-in WordPress upload filesize repair is review-only');
assert_true($backup_filesize_recorded, 'media validator filesize audit payload identifies the affected backup file');
assert_true(str_contains($filesize_preview, (string)$backup_filesize_drift_id), 'media validator filesize audit includes the backup filesize attachment ID');

Expand Down
Loading
Loading