| @@ -175,15 +175,30 @@ | ||
| 175 | 175 | $truncated = $total_count > $export_cap; |
| 176 | 176 | $donations = Donations::get_for_export( $filters, $export_cap, 0 ); |
| 177 | 177 | |
| 178 | 178 | // First pass: collect the union of custom-field labels so every row |
| 179 | - // shares one consistent set of trailing columns. | |
| 180 | - $field_labels = []; | |
| 179 | + // shares one consistent set of trailing columns. The label is | |
| 180 | + // attacker-controlled (submitted for any slug absent from the saved | |
| 181 | + // form), so the union is capped and looked up by key rather than | |
| 182 | + // `in_array()` — otherwise a form fed thousands of distinct labels | |
| 183 | + // turns this into an O(field_count x label_count) scan and a | |
| 184 | + // same-sized column set. Labels past the cap are not dropped; they are | |
| 185 | + // exported instead in a single trailing JSON column below. | |
| 186 | + $max_field_columns = 50; | |
| 187 | + $field_labels = []; | |
| 188 | + $field_label_index = []; | |
| 189 | + $fields_truncated = false; | |
| 181 | 190 | foreach ( $donations as $donation ) { |
| 182 | 191 | foreach ( $this->get_donation_custom_fields( $donation ) as $label => $value ) { |
| 183 | - if ( ! in_array( $label, $field_labels, true ) ) { | |
| 184 | - $field_labels[] = $label; | |
| 192 | + if ( isset( $field_label_index[ $label ] ) ) { | |
| 193 | + continue; | |
| 185 | 194 | } |
| 195 | + if ( count( $field_labels ) >= $max_field_columns ) { | |
| 196 | + $fields_truncated = true; | |
| 197 | + continue; | |
| 198 | + } | |
| 199 | + $field_label_index[ $label ] = true; | |
| 200 | + $field_labels[] = $label; | |
| 186 | 201 | } |
| 187 | 202 | } |
| 188 | 203 | |
| 189 | 204 | $rows = []; |
| @@ -188,9 +203,10 @@ | ||
| 188 | 203 | |
| 189 | 204 | $rows = []; |
| 190 | 205 | $rows[] = array_merge( |
| 191 | 206 | Column_Map::standard_donation_export_labels(), |
| 192 | - $field_labels | |
| 207 | + $field_labels, | |
| 208 | + $fields_truncated ? [ Column_Map::other_fields_export_label() ] : [] | |
| 193 | 209 | ); |
| 194 | 210 | |
| 195 | 211 | $title_cache = []; |
| 196 | 212 | foreach ( $donations as $donation ) { |
| @@ -240,8 +256,13 @@ | ||
| 240 | 256 | |
| 241 | 257 | $field_values = $this->get_donation_custom_fields( $donation ); |
| 242 | 258 | foreach ( $field_labels as $label ) { |
| 243 | 259 | $row[] = $field_values[ $label ] ?? ''; |
| 260 | + } | |
| 261 | + | |
| 262 | + if ( $fields_truncated ) { | |
| 263 | + $overflow = array_diff_key( $field_values, $field_label_index ); | |
| 264 | + $row[] = ! empty( $overflow ) ? wp_json_encode( $overflow ) : ''; | |
| 244 | 265 | } |
| 245 | 266 | |
| 246 | 267 | $rows[] = $row; |
| 247 | 268 | } |