From 64b1e7d0d5b0d5e9fe3a008af654f7beb481658b Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Fri, 1 May 2026 12:38:14 -0400 Subject: [PATCH 1/2] Bug 2036191 - Crash Signature Field Mismatch in Bugzilla REST API --- Bugzilla/Bug.pm | 4 +++- Bugzilla/WebService/Bug.pm | 29 +++++++++++++++++++++++++++++ extensions/BMO/lib/Data.pm | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index 9a84fb4467..7e382a1463 100644 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -2700,7 +2700,9 @@ sub set_all { # And set custom fields. my @custom_fields - = grep { $_->type != FIELD_TYPE_EXTENSION } Bugzilla->active_custom_fields; + = grep { $_->type != FIELD_TYPE_EXTENSION } Bugzilla->active_custom_fields( + {product => $self->product_obj, component => $self->component_obj} + ); foreach my $field (@custom_fields) { my $fname = $field->name; if (exists $params->{$fname}) { diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index 812a117ccd..0ae48531ac 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -1747,6 +1747,35 @@ sub _bug_to_hash { } } + # Include stored values for CFs not enabled for this product/component. + # Data is always loaded from DB (see DB_COLUMNS); we just surface it here + # when the value is non-empty so callers aren't silently missing data. + my %seen_cf = map { $_->name => 1 } @custom_fields; + my @hidden_cfs = grep { !$seen_cf{$_->name} && $_->type != FIELD_TYPE_EXTENSION } + Bugzilla->active_custom_fields({skip_extensions => 1}); + foreach my $field (@hidden_cfs) { + my $name = $field->name; + next if !filter_wants($params, $name, ['default', 'custom']); + if ($field->type == FIELD_TYPE_MULTI_SELECT) { + my @values = @{$bug->$name}; + next unless @values; + $item{$name} = [map { $self->type('string', $_) } @values]; + } + else { + my $value = $bug->$name; + next if !defined($value) || $value eq ''; + if ($field->type == FIELD_TYPE_BUG_ID) { + $item{$name} = $self->type('int', $value); + } + elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) { + $item{$name} = $self->type('dateTime', $value); + } + else { + $item{$name} = $self->type('string', $value); + } + } + } + # Timetracking fields are only sent if the user can see them. if ($user->is_timetracker) { if (filter_wants $params, 'estimated_time') { diff --git a/extensions/BMO/lib/Data.pm b/extensions/BMO/lib/Data.pm index 2e2e8a893e..adf8b28afe 100644 --- a/extensions/BMO/lib/Data.pm +++ b/extensions/BMO/lib/Data.pm @@ -149,6 +149,7 @@ tie( "Testing" => [], "Thunderbird" => [], "Toolkit" => [], + "Web Compatibility" => [], "WebExtensions" => [], }, qr/^cf_due_date$/ => { From afc393456f99e018762006c4582e3fa376f8571f Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Fri, 1 May 2026 13:50:26 -0400 Subject: [PATCH 2/2] Copilot suggested fixes --- Bugzilla/WebService/Bug.pm | 67 +++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index 0ae48531ac..fe5dda2b72 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -1731,49 +1731,26 @@ sub _bug_to_hash { foreach my $field (@custom_fields) { my $name = $field->name; next if !filter_wants($params, $name, ['default', 'custom']); - if ($field->type == FIELD_TYPE_BUG_ID) { - $item{$name} = $self->type('int', $bug->$name); - } - elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) { - my $value = $bug->$name; - $item{$name} = defined($value) ? $self->type('dateTime', $value) : undef; - } - elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - my @values = map { $self->type('string', $_) } @{$bug->$name}; - $item{$name} = \@values; - } - else { - $item{$name} = $self->type('string', $bug->$name); - } + $item{$name} = $self->_format_cf_value($field, $bug->$name); } - # Include stored values for CFs not enabled for this product/component. - # Data is always loaded from DB (see DB_COLUMNS); we just surface it here - # when the value is non-empty so callers aren't silently missing data. + # Include stored values for CFs not enabled for this product/component, + # so callers aren't silently missing data when a bug retains a value after + # its product/component changed. Multi-select CFs are excluded: they are + # not preloaded by DB_COLUMNS and each accessor fires a separate SELECT, + # making them too expensive to include for hidden fields across many bugs. my %seen_cf = map { $_->name => 1 } @custom_fields; - my @hidden_cfs = grep { !$seen_cf{$_->name} && $_->type != FIELD_TYPE_EXTENSION } - Bugzilla->active_custom_fields({skip_extensions => 1}); + my @hidden_cfs = grep { + !$seen_cf{$_->name} + && $_->type != FIELD_TYPE_EXTENSION + && $_->type != FIELD_TYPE_MULTI_SELECT + } Bugzilla->active_custom_fields({skip_extensions => 1}); foreach my $field (@hidden_cfs) { my $name = $field->name; next if !filter_wants($params, $name, ['default', 'custom']); - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - my @values = @{$bug->$name}; - next unless @values; - $item{$name} = [map { $self->type('string', $_) } @values]; - } - else { - my $value = $bug->$name; - next if !defined($value) || $value eq ''; - if ($field->type == FIELD_TYPE_BUG_ID) { - $item{$name} = $self->type('int', $value); - } - elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) { - $item{$name} = $self->type('dateTime', $value); - } - else { - $item{$name} = $self->type('string', $value); - } - } + my $raw = $bug->$name; + next if !defined($raw) || $raw eq ''; + $item{$name} = $self->_format_cf_value($field, $raw); } # Timetracking fields are only sent if the user can see them. @@ -1829,6 +1806,22 @@ sub _bug_to_hash { return \%item; } +sub _format_cf_value { + my ($self, $field, $value) = @_; + if ($field->type == FIELD_TYPE_BUG_ID) { + return $self->type('int', $value); + } + elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) { + return defined($value) ? $self->type('dateTime', $value) : undef; + } + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + return [map { $self->type('string', $_) } @{$value}]; + } + else { + return $self->type('string', $value); + } +} + sub _user_to_hash { my ($self, $user, $filters, $types, $prefix) = @_; my $item = filter $filters,