summaryrefslogtreecommitdiffstats
path: root/attachment.cgi
blob: 8fcac0b8859c50a632184650810a446e89fd8841 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
#!/usr/bonsaitools/bin/perl -w
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Terry Weissman <terry@mozilla.org>
#                 Myk Melez <myk@mozilla.org>

################################################################################
# Script Initialization
################################################################################

# Make it harder for us to do dangerous things in Perl.
use diagnostics;
use strict;

# Include the Bugzilla CGI and general utility library.
require "CGI.pl";

# Establish a connection to the database backend.
ConnectToDatabase();

# Use the template toolkit (http://www.template-toolkit.org/) to generate
# the user interface (HTML pages and mail messages) using templates in the
# "template/" subdirectory.
use Template;

# Create the global template object that processes templates and specify
# configuration parameters that apply to all templates processed in this script.
my $template = Template->new(
  {
    # Colon-separated list of directories containing templates.
    INCLUDE_PATH => "template/default" ,
    # Allow templates to be specified with relative paths.
    RELATIVE => 1 
  }
);

# Define the global variables and functions that will be passed to the UI 
# template.  Individual functions add their own values to this hash before
# sending them to the templates they process.
my $vars = 
  {
    # Function for retrieving global parameters.
    'Param' => \&Param , 

    # Function for processing global parameters that contain references
    # to other global parameters.
    'PerformSubsts' => \&PerformSubsts
  };

# Check whether or not the user is logged in and, if so, set the $::userid 
# and $::usergroupset variables.
quietly_check_login();

################################################################################
# Main Body Execution
################################################################################

# All calls to this script should contain an "action" variable whose value
# determines what the user wants to do.  The code below checks the value of
# that variable and runs the appropriate code.

# Determine whether to use the action specified by the user or the default.
my $action = $::FORM{'action'} || 'view';

if ($action eq "view")  
{ 
  validateID();
  view(); 
}
elsif ($action eq "viewall") 
{ 
  ValidateBugID($::FORM{'bugid'});
  viewall(); 
}
elsif ($action eq "edit") 
{ 
  validateID();
  edit(); 
}
elsif ($action eq "update") 
{ 
  confirm_login();
  UserInGroup("editbugs")
    || DisplayError("You are not authorized to edit attachments.")
    && exit;
  validateID();
  validateDescription();
  validateMIMEType();
  validateIsPatch();
  validateIsObsolete();
  validateStatuses();
  update();
}
else 
{ 
  DisplayError("I could not figure out what you wanted to do.")
}

exit;

################################################################################
# Data Validation / Security Authorization
################################################################################

sub validateID
{
  # Validate the value of the "id" form field, which must contain a positive
  # integer that is the ID of an existing attachment.

  $::FORM{'id'} =~ /^[1-9][0-9]*$/
    || DisplayError("You did not enter a valid attachment number.") 
      && exit;
  
  # Make sure the attachment exists in the database.
  SendSQL("SELECT bug_id FROM attachments WHERE attach_id = $::FORM{'id'}");
  MoreSQLData()
    || DisplayError("Attachment #$::FORM{'id'} does not exist.") 
    && exit;

  # Make sure the user is authorized to access this attachment's bug.
  my ($bugid) = FetchSQLData();
  ValidateBugID($bugid);
}

sub validateDescription
{
  $::FORM{'description'}
    || DisplayError("You must enter a description for the attachment.")
      && exit;
}

sub validateMIMEType
{
  $::FORM{'mimetype'} =~ /^(application|audio|image|message|model|multipart|text|video)\/.+$/
    || DisplayError("You must enter a valid MIME type of the form <em>foo/bar</em>
                     where <em>foo</em> is either <em>application, audio, image, message, 
                     model, multipart, text,</em> or <em>video</em>.")
      && exit;
}

sub validateIsPatch
{
  # Set the ispatch flag to zero if it is undefined, since the UI uses
  # an HTML checkbox to represent this flag, and unchecked HTML checkboxes
  # do not get sent in HTML requests.
  $::FORM{'ispatch'} = $::FORM{'ispatch'} ? 1 : 0;
}

sub validateIsObsolete
{
  # Set the isobsolete flag to zero if it is undefined, since the UI uses
  # an HTML checkbox to represent this flag, and unchecked HTML checkboxes
  # do not get sent in HTML requests.
  $::FORM{'isobsolete'} = $::FORM{'isobsolete'} ? 1 : 0;
}

sub validateStatuses
{
  # Get a list of attachment statuses that are valid for this attachment.
  PushGlobalSQLState();
  SendSQL("SELECT  attachstatusdefs.id
           FROM    attachments, bugs, attachstatusdefs
           WHERE   attachments.attach_id = $::FORM{'id'}
           AND     attachments.bug_id = bugs.bug_id
           AND     attachstatusdefs.product = bugs.product");
  my @statusdefs;
  push(@statusdefs, FetchSQLData()) while MoreSQLData();
  PopGlobalSQLState();
  
  foreach my $status (@{$::MFORM{'status'}})
  {
    grep($_ == $status, @statusdefs)
      || DisplayError("One of the statuses you entered is not a valid status
                       for this attachment.")
        && exit;
  }
}

################################################################################
# Functions
################################################################################

sub view
{
  # Display an attachment.

  # Retrieve the attachment content and its MIME type from the database.
  SendSQL("SELECT mimetype, thedata FROM attachments WHERE attach_id = $::FORM{'id'}");
  my ($mimetype, $thedata) = FetchSQLData();
    
  # Return the appropriate HTTP response headers.
  print "Content-Type: $mimetype\n\n";

  print $thedata;
}


sub viewall
{
  # Display all attachments for a given bug in a series of IFRAMEs within one HTML page.

  # Retrieve the attachments from the database and write them into an array
  # of hashes where each hash represents one attachment.
  SendSQL("SELECT attach_id, creation_ts, mimetype, description, ispatch, isobsolete 
           FROM attachments WHERE bug_id = $::FORM{'bugid'} ORDER BY attach_id");
  my @attachments; # the attachments array
  while ( MoreSQLData() ) {
    my %a; # the attachment hash
    ($a{'attachid'}, $a{'date'}, $a{'mimetype'}, 
     $a{'description'}, $a{'ispatch'}, $a{'isobsolete'}) = FetchSQLData();

    # Format the attachment's creation/modification date into something readable.
    if ($a{'date'} =~ /^(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/) {
        $a{'date'} = "$3/$4/$2&nbsp;$5:$6";
    }

    # Quote HTML characters (&<>) in the description and MIME Type.
    $a{'description'} = value_quote($a{'description'});
    $a{'mimetype'} = value_quote($a{'mimetype'});

    # Flag attachments as to whether or not they can be viewed (as opposed to
    # being downloaded).  Currently I decide they are viewable if their MIME type 
    # is either text/*, image/*, or application/vnd.mozilla.*.
    # !!! Yuck, what an ugly hack.  Fix it!
    $a{'isviewable'} = ( $a{'mimetype'} =~ /^(text|image|application\/vnd\.mozilla\.)/ );

    # Retrieve a list of status flags that have been set on the attachment.
    PushGlobalSQLState();
    SendSQL("SELECT    name 
             FROM      attachstatuses, attachstatusdefs 
             WHERE     attach_id = $a{'attachid'} 
             AND       attachstatuses.statusid = attachstatusdefs.id
             ORDER BY  sortkey");
    my @statuses;
    push(@statuses, FetchSQLData()) while MoreSQLData();
    $a{'statuses'} = \@statuses;
    PopGlobalSQLState();

    # Add the hash representing the attachment to the array of attachments.
    push @attachments, \%a;
  }

  # Retrieve the bug summary for displaying on screen.
  SendSQL("SELECT short_desc FROM bugs WHERE bug_id = $::FORM{'bugid'}");
  my ($bugsummary) = FetchSQLData();

  # Define the variables and functions that will be passed to the UI template.
  $vars->{'bugid'} = $::FORM{'bugid'};
  $vars->{'bugsummary'} = $bugsummary;
  $vars->{'attachments'} = \@attachments;

  # Return the appropriate HTTP response headers.
  print "Content-Type: text/html\n\n";

  # Generate and return the UI (HTML page) from the appropriate template.
  $template->process("attachment/viewall.atml", $vars)
    || DisplayError("Template process failed: " . $template->error())
    && exit;
}


sub edit
{
  # Edit an attachment record.  Users with "editbugs" privileges can edit the 
  # attachment's description, MIME type, ispatch and isobsolete flags, and 
  # statuses, and they can also submit a comment that appears in the bug.  
  # Users cannot edit the content of the attachment itself.

  # Retrieve the attachment from the database.
  SendSQL("SELECT description, mimetype, bug_id, ispatch, isobsolete 
           FROM attachments WHERE attach_id = $::FORM{'id'}");
  my ($description, $mimetype, $bugid, $ispatch, $isobsolete) = FetchSQLData();

  # Flag attachment as to whether or not it can be viewed (as opposed to
  # being downloaded).  Currently I decide it is viewable if its MIME type 
  # is either text/.* or application/vnd.mozilla.*.
  # !!! Yuck, what an ugly hack.  Fix it!
  my $isviewable = ( $mimetype =~ /^(text|image|application\/vnd\.mozilla\.)/ );

  # Retrieve a list of status flags that have been set on the attachment.
  my %statuses;
  SendSQL("SELECT  id, name 
           FROM    attachstatuses JOIN attachstatusdefs 
           WHERE   attachstatuses.statusid = attachstatusdefs.id 
           AND     attach_id = $::FORM{'id'}");
  while ( my ($id, $name) = FetchSQLData() )
  {
    $statuses{$id} = $name;
  }

  # Retrieve a list of statuses for this bug's product, and build an array 
  # of hashes in which each hash is a status flag record.
  # ???: Move this into versioncache or its own routine?
  my @statusdefs;
  SendSQL("SELECT   id, name 
           FROM     attachstatusdefs, bugs 
           WHERE    bug_id = $bugid 
           AND      attachstatusdefs.product = bugs.product 
           ORDER BY sortkey");
  while ( MoreSQLData() )
  {
    my ($id, $name) = FetchSQLData();
    push @statusdefs, { 'id' => $id , 'name' => $name };
  }

  # Retrieve a list of attachments for this bug as well as a summary of the bug
  # to use in a navigation bar across the top of the screen.
  SendSQL("SELECT attach_id FROM attachments WHERE bug_id = $bugid ORDER BY attach_id");
  my @bugattachments;
  push(@bugattachments, FetchSQLData()) while (MoreSQLData());
  SendSQL("SELECT short_desc FROM bugs WHERE bug_id = $bugid");
  my ($bugsummary) = FetchSQLData();

  # Define the variables and functions that will be passed to the UI template.
  $vars->{'attachid'} = $::FORM{'id'}; 
  $vars->{'description'} = $description; 
  $vars->{'mimetype'} = $mimetype; 
  $vars->{'bugid'} = $bugid; 
  $vars->{'bugsummary'} = $bugsummary; 
  $vars->{'ispatch'} = $ispatch; 
  $vars->{'isobsolete'} = $isobsolete; 
  $vars->{'isviewable'} = $isviewable; 
  $vars->{'statuses'} = \%statuses; 
  $vars->{'statusdefs'} = \@statusdefs; 
  $vars->{'attachments'} = \@bugattachments; 

  # Return the appropriate HTTP response headers.
  print "Content-Type: text/html\n\n";

  # Generate and return the UI (HTML page) from the appropriate template.
  $template->process("attachment/edit.atml", $vars)
    || DisplayError("Template process failed: " . $template->error())
    && exit;
}


sub update
{
  # Update an attachment record.

  # Get the bug ID for the bug to which this attachment is attached.
  SendSQL("SELECT bug_id FROM attachments WHERE attach_id = $::FORM{'id'}");
  my $bugid = FetchSQLData() 
    || DisplayError("Cannot figure out bug number.")
    && exit;

  # Lock database tables in preparation for updating the attachment.
  SendSQL("LOCK TABLES attachments WRITE , attachstatuses WRITE , 
           attachstatusdefs READ , fielddefs READ , bugs_activity WRITE");

  # Get a copy of the attachment record before we make changes
  # so we can record those changes in the activity table.
  SendSQL("SELECT description, mimetype, ispatch, isobsolete 
           FROM attachments WHERE attach_id = $::FORM{'id'}");
  my ($olddescription, $oldmimetype, $oldispatch, $oldisobsolete) = FetchSQLData();

  # Get the list of old status flags.
  SendSQL("SELECT    attachstatusdefs.name 
           FROM      attachments, attachstatuses, attachstatusdefs
           WHERE     attachments.attach_id = $::FORM{'id'}
           AND       attachments.attach_id = attachstatuses.attach_id
           AND       attachstatuses.statusid = attachstatusdefs.id
           ORDER BY  attachstatusdefs.sortkey
          ");
  my @oldstatuses;
  while (MoreSQLData()) {
    push(@oldstatuses, FetchSQLData());
  }
  my $oldstatuslist = join(', ', @oldstatuses);

  # Update the database with the new status flags.
  SendSQL("DELETE FROM attachstatuses WHERE attach_id = $::FORM{'id'}");
  foreach my $statusid (@{$::MFORM{'status'}}) 
  {
    SendSQL("INSERT INTO attachstatuses (attach_id, statusid) VALUES ($::FORM{'id'}, $statusid)");
  }

  # Get the list of new status flags.
  SendSQL("SELECT    attachstatusdefs.name 
           FROM      attachments, attachstatuses, attachstatusdefs
           WHERE     attachments.attach_id = $::FORM{'id'}
           AND       attachments.attach_id = attachstatuses.attach_id
           AND       attachstatuses.statusid = attachstatusdefs.id
           ORDER BY  attachstatusdefs.sortkey
          ");
  my @newstatuses;
  while (MoreSQLData()) {
    push(@newstatuses, FetchSQLData());
  }
  my $newstatuslist = join(', ', @newstatuses);

  # Quote "description" and "mimetype" for use in the SQL UPDATE statement.
  my $quoteddescription = SqlQuote($::FORM{'description'});
  my $quotedmimetype = SqlQuote($::FORM{'mimetype'});

  # Update the attachment record in the database.
  # Sets the creation timestamp to itself to avoid it being updated automatically.
  SendSQL("UPDATE  attachments 
           SET     description = $quoteddescription , 
                   mimetype = $quotedmimetype , 
                   ispatch = $::FORM{'ispatch'} , 
                   isobsolete = $::FORM{'isobsolete'} , 
                   creation_ts = creation_ts
           WHERE   attach_id = $::FORM{'id'}
         ");

  # Record changes in the activity table.
  if ($olddescription ne $::FORM{'description'}) {
    my $quotedolddescription = SqlQuote($olddescription);
    my $fieldid = GetFieldID('attachments.description');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
             VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedolddescription, $quoteddescription)");
  }
  if ($oldmimetype ne $::FORM{'mimetype'}) {
    my $quotedoldmimetype = SqlQuote($oldmimetype);
    my $fieldid = GetFieldID('attachments.mimetype');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
             VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedoldmimetype, $quotedmimetype)");
  }
  if ($oldispatch ne $::FORM{'ispatch'}) {
    my $fieldid = GetFieldID('attachments.ispatch');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
             VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $oldispatch, $::FORM{'ispatch'})");
  }
  if ($oldisobsolete ne $::FORM{'isobsolete'}) {
    my $fieldid = GetFieldID('attachments.isobsolete');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
             VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $oldisobsolete, $::FORM{'isobsolete'})");
  }
  if ($oldstatuslist ne $newstatuslist) {
    my ($removed, $added) = DiffStrings($oldstatuslist, $newstatuslist);
    my $quotedremoved = SqlQuote($removed);
    my $quotedadded = SqlQuote($added);
    my $fieldid = GetFieldID('attachstatusdefs.name');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
             VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedremoved, $quotedadded)");
  }

  # Unlock all database tables now that we are finished updating the database.
  SendSQL("UNLOCK TABLES");

  # If this installation has enabled the request manager, let the manager know
  # an attachment was updated so it can check for requests on that attachment
  # and fulfill them.  The request manager allows users to request database
  # changes of other users and tracks the fulfillment of those requests.  When
  # an attachment record is updated and the request manager is called, it will
  # fulfill those requests that were requested of the user performing the update
  # which are requests for the attachment being updated.
  #my $requests;
  #if (Param('userequestmanager'))
  #{
  #  use Request;
  #  # Specify the fieldnames that have been updated.
  #  my @fieldnames = ('description', 'mimetype', 'status', 'ispatch', 'isobsolete');
  #  # Fulfill pending requests.
  #  $requests = Request::fulfillRequest('attachment', $::FORM{'id'}, @fieldnames);
  #  $vars->{'requests'} = $requests; 
  #}

  # If the user submitted a comment while editing the attachment, 
  # add the comment to the bug.
  if ( $::FORM{'comment'} )
  {
    use Text::Wrap;
    $Text::Wrap::columns = 80;
    $Text::Wrap::huge = 'wrap';

    # Append a string to the comment to let users know that the comment came from
    # the "edit attachment" screen.
    my $comment = qq|(From update of attachment $::FORM{'id'})\n| . $::FORM{'comment'};

    my $wrappedcomment = "";
    foreach my $line (split(/\r\n|\r|\n/, $comment))
    {
      if ( $line =~ /^>/ )
      {
        $wrappedcomment .= $line . "\n";
      }
      else
      {
        $wrappedcomment .= wrap('', '', $line) . "\n";
      }
    }

    # Get the user's login name since the AppendComment function needs it.
    my $who = DBID_to_name($::userid);
    # Mention $::userid again so Perl doesn't give me a warning about it.
    my $neverused = $::userid;

    # Append the comment to the list of comments in the database.
    AppendComment($bugid, $who, $wrappedcomment);

  }

  # Send mail to let people know the bug has changed.  Uses a special syntax
  # of the "open" and "exec" commands to capture the output of "processmail",
  # which "system" doesn't allow, without running the command through a shell,
  # which backticks (``) do.
  #system ("./processmail", $bugid , $::userid);
  #my $mailresults = `./processmail $bugid $::userid`;
  my $mailresults = '';
  open(PMAIL, "-|") or exec('./processmail', $bugid, DBID_to_name($::userid));
  $mailresults .= $_ while <PMAIL>;
  close(PMAIL);
 
  # Define the variables and functions that will be passed to the UI template.
  $vars->{'attachid'} = $::FORM{'id'}; 
  $vars->{'bugid'} = $bugid; 
  $vars->{'mailresults'} = $mailresults; 

  # Return the appropriate HTTP response headers.
  print "Content-Type: text/html\n\n";

  # Generate and return the UI (HTML page) from the appropriate template.
  $template->process("attachment/updated.atml", $vars)
    || DisplayError("Template process failed: " . $template->error())
    && exit;

}