]> git.imager.perl.org - bse.git/blame - site/cgi-bin/modules/BSE/MetaMeta.pm
fix infinite loop with empty comma
[bse.git] / site / cgi-bin / modules / BSE / MetaMeta.pm
CommitLineData
f5b7b326
TC
1package BSE::MetaMeta;
2use strict;
3use Carp qw(confess);
4use Image::Size;
c29434c9 5use Fcntl ':seek';
f5b7b326 6
b4c02bf9 7our $VERSION = "1.005";
f5b7b326 8
4029e8ab
TC
9=head1 NAME
10
11BSE::MetaMeta - information about metadata.
12
13=head1 SYNOPSIS
14
15 my @metainfo = $class->all_metametadata;
16 ...
17
18=head1 INSTANCE METHODS
19
20=over
21
22=cut
23
24my %meta_rules;
f5b7b326
TC
25
26my %rule_map =
27 (
4029e8ab 28 image => "image",
f5b7b326
TC
29 integer => "integer",
30 string => "dh_one_line",
4029e8ab 31 real => "real",
f5b7b326
TC
32 enum => "meta_enum", # generated
33 );
34
4029e8ab
TC
35my %field_defs =
36 (
37 image =>
38 {
c29434c9 39 type => "image",
4029e8ab
TC
40 htmltype => "file",
41 },
42 string =>
43 {
44 htmltype => "text",
45 width => 60,
46 },
47 text =>
48 {
49 htmltype => "textarea",
50 width => 60,
51 height => 20,
52 },
53 integer =>
54 {
55 htmltype => "text",
56 width => 8,
57 },
58 real =>
59 {
60 htmltype => "text",
61 width => 10,
62 },
63 enum =>
64 {
65 htmltype => "select",
66 },
67 );
f5b7b326 68
4029e8ab 69=item name
f5b7b326 70
4029e8ab 71The field name of the metadata.
f5b7b326 72
4029e8ab 73=cut
f5b7b326 74
4029e8ab 75sub name { $_[0]{name} }
f5b7b326 76
4029e8ab 77=item type
f5b7b326 78
4029e8ab 79The type of the metadata.
f5b7b326 80
4029e8ab 81=cut
f5b7b326
TC
82
83sub type { $_[0]{type} }
84
4029e8ab
TC
85=item title
86
87The display name of the metadata.
88
89=cut
90
f5b7b326
TC
91sub title { $_[0]{title} }
92
4029e8ab
TC
93=item rules
94
95The validation rules for the metadata.
96
97=cut
98
f5b7b326
TC
99sub rules { @{$_[0]{rules}} }
100
4029e8ab
TC
101=item values
102
103The permitted values for the metadata for enum types.
104
105=cut
106
f5b7b326
TC
107sub values { @{$_[0]{values}} }
108
4029e8ab
TC
109=item labels
110
111The display labels as a list.
112
113=cut
114
f5b7b326
TC
115sub labels { @{$_[0]{labels}} }
116
4029e8ab
TC
117=item htmltype
118
119How to display this field. May be ignored depending on C<type>.
120
121=cut
122
123sub htmltype { $_[0]{htmltype} }
124
da301b75
TC
125=item fieldtype
126
127How to parse this field. May be ignored depending on type.
128
129=cut
130
131sub fieldtype { $_[0]{fieldtype} }
132
4029e8ab
TC
133=item width
134
135Display width. May be ignored depending on C<type>.
136
137=cut
138
139sub width { $_[0]{width} }
140
141=item height
142
143Display height. May be ignored depending on C<type>.
144
145=cut
146
147sub height { $_[0]{height} }
148
149=item ro
150
151Whether this field is read-only.
152
153=cut
154
f5b7b326
TC
155sub ro { $_[0]{ro} }
156
4029e8ab
TC
157=item unit
158
159Unit of measurement of this field (for display only)
160
161=cut
162
f5b7b326
TC
163sub unit { $_[0]{unit} }
164
4029e8ab
TC
165=item is_text
166
167True if this is representable as text.
168
169=cut
170
f5b7b326
TC
171sub is_text {
172 $_[0]{type} ne "image";
173}
174
4029e8ab
TC
175=item cond
176
177True if the field should be prompted for if not present.
178
179=cut
180
f5b7b326
TC
181sub cond {
182 my ($self, $file) = @_;
183
184 return $self->{cond}->($file);
185}
186
4029e8ab
TC
187=item field
188
189Return a hash suitable as the validation parameter for the field (and
190for template field formatting).
191
192=cut
193
194sub field {
195 my ($self) = @_;
196
197 my %field =
198 (
4029e8ab
TC
199 description => scalar $self->title,
200 units => scalar $self->unit,
c29434c9 201 rules => [ $self->rules ],
da301b75 202 rawtype => scalar $self->type,
4029e8ab 203 htmltype => scalar $self->htmltype,
da301b75
TC
204 type => scalar $self->fieldtype,
205 width => scalar $self->width,
206 height => scalar $self->height,
4029e8ab 207 );
da301b75
TC
208 my $defs = $field_defs{$self->type};
209 for my $key (keys %$defs) {
210 defined $field{$key} or $field{$key} = $defs->{$key};
211 }
4029e8ab
TC
212 if ($self->type =~ /^(?:multi)?enum$/) {
213 my $values = [ $self->values ];
214 my $labels = [ $self->labels ];
215 my @values = map
216 +{ id => $values->[$_], label => $labels->[$_] },
217 0 .. $#$values;
218 $field{select} =
219 {
220 id => "id",
221 label => "label",
222 values => \@values,
223 };
224 }
da301b75 225 my %fields = ( $self->name => \%field );
4029e8ab 226
da301b75
TC
227 require BSE::Validate;
228 my $configured =
229 BSE::Validate::bse_configure_fields({ $self->name => \%fields }, BSE::Cfg->single,
230 $self->validation_section);
231
232 return $fields{$self->name};
4029e8ab
TC
233}
234
b4c02bf9 235=item validate
4029e8ab 236
b4c02bf9 237Validate a meta data item.
4029e8ab
TC
238
239=cut
240
f5b7b326
TC
241sub validate {
242 my ($self, %opts) = @_;
243
244 my $value = delete $opts{value};
245 defined $value
246 or confess "value not supplied\n";
247 my $rerror = delete $opts{error}
248 or confess "error ref not supplied\n";
4029e8ab 249 my $section = $self->validation_section;
f5b7b326
TC
250
251 # kind of clumsy
252 require DevHelp::Validate;
253 my @field_rules = $self->rules;
254 $rule_map{$self->type} && unshift @field_rules, $rule_map{$self->type};
255 my %values =
256 (
257 value => $value
258 );
259 my %fields =
260 (
261 value =>
262 {
263 rules => \@field_rules,
264 description => $self->title,
265 },
266 );
267 my %rules = %meta_rules;
268 if ($self->type eq "enum") {
269 $rules{meta_enum} =
270 {
271 match => "^(?:" . join("|", map quotemeta, $self->values) . ")\\z",
272 error => '$n must be one of ' . join(", ", $self->values),
273 };
274 }
275
276 my $val = DevHelp::Validate::Hash->new
277 (
278 fields => \%fields,
279 rules => \%rules,
280 cfg => $self->{cfg},
4029e8ab 281 section => $section,
f5b7b326
TC
282 );
283 my %errors;
284 $val->validate(\%values, \%errors);
285 if (keys %errors) {
286 $$rerror = $errors{value};
287 return;
288 }
289
290 return 1;
291}
292
b4c02bf9 293=item metanames
4029e8ab 294
b4c02bf9 295List of form fields that are read for the meta item.
4029e8ab
TC
296
297=cut
298
f5b7b326
TC
299sub metanames {
300 my ($self) = @_;
301
302 if ($self->type eq 'image') {
c29434c9 303 return ( $self->data_name, $self->width_name, $self->height_name, $self->display_name );
f5b7b326
TC
304 }
305 else {
306 return $self->name;
307 }
308}
309
4029e8ab
TC
310=item data_name
311
312The field name of the metadata.
313
314=cut
315
f5b7b326
TC
316sub data_name {
317 $_[0]{data_name}
318}
319
4029e8ab
TC
320=item width_name
321
322Where width information is stored for this image
323
324=cut
325
f5b7b326
TC
326sub width_name {
327 $_[0]{width_name}
328}
329
4029e8ab
TC
330=item height_name
331
332Where height information is stored for this image.
333
334=cut
335
f5b7b326
TC
336sub height_name {
337 $_[0]{height_name}
338}
339
4029e8ab
TC
340=item display_name
341
342Where the original filename is stored for the image.
343
344=cut
345
346sub display_name {
347 $_[0]{display_name}
348}
349
b4c02bf9
TC
350=back
351
4029e8ab
TC
352=head1 CLASS METHODS
353
354=over
355
356=item new
357
358=cut
359
360sub new {
361 my $class = shift;
362 my %opts =
363 (
364 rules => '',
365 ro => 0,
366 values => [],
367 cond => "1",
368 type => "string",
369 unit => '',
370 help => '',
da301b75 371 fieldtype => "",
4029e8ab
TC
372 @_
373 );
374
da301b75
TC
375
376
4029e8ab
TC
377 $opts{cfg} && $opts{cfg}->can("entry")
378 or confess "Missing or invalid cfg parameter";
379 $opts{name}
380 or confess "Missing name parameter";
381 $opts{name} =~ /^[A-Za-z_][A-Za-z0-9_-]*$/
382 or confess "Invalid metadata name parameter";
383
384 $field_defs{$opts{type}}
385 or confess "Unknown metadata type '$opts{type}' for field '$opts{name}'";
386
387 my $name = $opts{name};
388 for my $subkey (qw/data width height display/) {
389 my $key = $subkey . "_name";
390 defined $opts{$key} or $opts{$key} = $name . "_" . $subkey;
391 }
392 $opts{title} ||= $name;
393
394 if ($opts{type} =~ /^(?:multi)?enum/) {
395 if ($opts{values}) {
396 unless (ref $opts{values}) {
397 $opts{values} = [ split /;/, $opts{values} ];
398 }
399 @{$opts{values}}
400 or confess "$opts{name} has enum type but no values";
401 }
402 else {
403 confess "$opts{name} has enum type but no values";
404 }
405
406 if ($opts{labels}) {
407 unless (ref $opts{labels}) {
408 $opts{labels} = [ split /;/, $opts{labels} ];
409 }
410 @{$opts{labels}}
411 or confess "$opts{name} has enum type but no labels";
412 }
413 else {
414 $opts{labels} = $opts{values};
415 }
416 }
417
418 $opts{htmltype} ||= $field_defs{$opts{type}}{htmltype};
419
420 ref $opts{rules} or $opts{rules} = [ split /[,;]/, $opts{rules} ];
c29434c9 421 unshift @{$opts{rules}}, $rule_map{$opts{type}};
4029e8ab
TC
422
423 if ($opts{cond}) {
424 my $code = $opts{cond};
425 $opts{cond} = eval 'sub { my $file = shift; my $obj = $file; ' . $code . ' }'
426 or die "Cannot compile condition code <$code> for $opts{name}: $@";
427 }
428 else {
429 $opts{cond} = sub { 1 };
430 }
431
432 bless \%opts, $class;
433}
434
f5b7b326 435sub keys {
da301b75 436 qw/title help rules ro values labels type data_name width_name height_name cond unit htmltype width height fieldtype/;
f5b7b326
TC
437}
438
439sub retrieve {
c29434c9 440 my ($class, $req, $owner, $errors, %opts) = @_;
f5b7b326 441
c29434c9 442 my $api = $opts{api};
f5b7b326
TC
443 my @meta;
444 my @meta_delete;
445 my $cgi = $req->cgi;
446 my @metafields = grep !$_->ro, $owner->metafields($req->cfg);
447 my %current_meta = map { $_ => 1 } $owner->metanames;
448 for my $meta (@metafields) {
449 my $name = $meta->name;
450 my $cgi_name = "meta_$name";
451 if ($cgi->param("delete_$cgi_name")) {
452 for my $metaname ($meta->metanames) {
453 push @meta_delete, $metaname
454 if $current_meta{$metaname};
455 }
456 }
457 else {
458 my $new;
c29434c9
TC
459 my %fields =
460 (
461 $cgi_name => $meta->field,
462 );
463 if ($req->validate(fields => \%fields,
464 rules => \%meta_rules,
465 errors => $errors)) {
466 my $values = $req->cgi_fields
467 (
468 fields => \%fields,
469 api => $api,
470 );
471 my $value = $values->{$cgi_name};
472 if ($meta->is_text) {
473 if (defined $value &&
474 ($value =~ /\S/ || $current_meta{$meta->name})) {
475 utf8::encode($value);
f5b7b326
TC
476 push @meta,
477 {
478 name => $name,
479 value => $value,
480 };
481 }
f5b7b326 482 }
c29434c9
TC
483 else {
484 if ($value) {
485 my $up = $value->{fh};
486 binmode $up;
487 seek $up, 0, SEEK_SET;
488 my $data = do { local $/; <$up> };
489 my ($width, $height, $type) = imgsize(\$data);
490
491 if ($width && $height) {
492 push @meta,
493 (
494 {
495 name => $meta->data_name,
496 value => $data,
497 content_type => "image/\L$type",
498 },
499 {
500 name => $meta->width_name,
501 value => $width,
502 },
503 {
504 name => $meta->height_name,
505 value => $height,
506 },
507 {
508 name => $meta->display_name,
509 value => "" . $value->{filename},
510 },
511 );
512 }
f5b7b326
TC
513 }
514 }
515 }
516 }
517 }
518
519 return { meta => \@meta, delete => \@meta_delete };
520}
521
522sub save {
523 my ($class, $owner, $meta) = @_;
524
c29434c9
TC
525 for my $meta_delete (@{$meta->{delete}}, map $_->{name}, @{$meta->{meta}}) {
526 $owner->delete_meta_by_name($meta_delete);
f5b7b326
TC
527 }
528 for my $meta (@{$meta->{meta}}) {
529 $owner->add_meta(%$meta, appdata => 1);
530 }
531
532 1;
533}
534
4029e8ab
TC
535sub all_metametadata {
536 my ($class, $cfg) = @_;
537
538 $cfg ||= BSE::Cfg->new;
539
540 my @metafields;
541 my @keys = $cfg->orderCS($class->fields_section);
542 for my $name (@keys) {
543 my %opts = ( name => $name );
544 my $section = $class->name_section($name);
545 for my $key ($class->keys) {
546 my $value = $cfg->entry($section, $key);
547 if (defined $value) {
548 $opts{$key} = $value;
549 }
550 }
551 push @metafields, $class->new(%opts, cfg => $cfg);
552 }
553
554 return @metafields;
555}
556
f5b7b326 5571;
4029e8ab
TC
558
559=back
560
561=cut