Initial patch.
[vuplus_webkit] / Websites / bugs.webkit.org / Bugzilla / Attachment.pm
diff --git a/Websites/bugs.webkit.org/Bugzilla/Attachment.pm b/Websites/bugs.webkit.org/Bugzilla/Attachment.pm
new file mode 100644 (file)
index 0000000..314227c
--- /dev/null
@@ -0,0 +1,959 @@
+# -*- 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>
+#                 Marc Schumann <wurblzap@gmail.com>
+#                 Frédéric Buclin <LpSolit@gmail.com>
+
+use strict;
+
+package Bugzilla::Attachment;
+
+=head1 NAME
+
+Bugzilla::Attachment - a file related to a bug that a user has uploaded
+                       to the Bugzilla server
+
+=head1 SYNOPSIS
+
+  use Bugzilla::Attachment;
+
+  # Get the attachment with the given ID.
+  my $attachment = Bugzilla::Attachment->get($attach_id);
+
+  # Get the attachments with the given IDs.
+  my $attachments = Bugzilla::Attachment->get_list($attach_ids);
+
+=head1 DESCRIPTION
+
+This module defines attachment objects, which represent files related to bugs
+that users upload to the Bugzilla server.
+
+=cut
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Flag;
+use Bugzilla::User;
+use Bugzilla::Util;
+use Bugzilla::Field;
+
+sub get {
+    my $invocant = shift;
+    my $id = shift;
+
+    my $attachments = _retrieve([$id]);
+    my $self = $attachments->[0];
+    bless($self, ref($invocant) || $invocant) if $self;
+
+    return $self;
+}
+
+sub get_list {
+    my $invocant = shift;
+    my $ids = shift;
+
+    my $attachments = _retrieve($ids);
+    foreach my $attachment (@$attachments) {
+        bless($attachment, ref($invocant) || $invocant);
+    }
+
+    return $attachments;
+}
+
+sub _retrieve {
+    my ($ids) = @_;
+
+    return [] if scalar(@$ids) == 0;
+
+    my @columns = (
+        'attachments.attach_id AS id',
+        'attachments.bug_id AS bug_id',
+        'attachments.description AS description',
+        'attachments.mimetype AS contenttype',
+        'attachments.submitter_id AS attacher_id',
+        Bugzilla->dbh->sql_date_format('attachments.creation_ts',
+                                       '%Y.%m.%d %H:%i') . " AS attached",
+        'attachments.modification_time',
+        'attachments.filename AS filename',
+        'attachments.ispatch AS ispatch',
+        'attachments.isurl AS isurl',
+        'attachments.isobsolete AS isobsolete',
+        'attachments.isprivate AS isprivate'
+    );
+    my $columns = join(", ", @columns);
+    my $dbh = Bugzilla->dbh;
+    my $records = $dbh->selectall_arrayref(
+                      "SELECT $columns
+                         FROM attachments
+                        WHERE " 
+                       . Bugzilla->dbh->sql_in('attach_id', $ids) 
+                 . " ORDER BY attach_id",
+                       { Slice => {} });
+    return $records;
+}
+
+=pod
+
+=head2 Instance Properties
+
+=over
+
+=item C<id>
+
+the unique identifier for the attachment
+
+=back
+
+=cut
+
+sub id {
+    my $self = shift;
+    return $self->{id};
+}
+
+=over
+
+=item C<bug_id>
+
+the ID of the bug to which the attachment is attached
+
+=back
+
+=cut
+
+# XXX Once Bug.pm slims down sufficiently this should become a reference
+# to a bug object.
+sub bug_id {
+    my $self = shift;
+    return $self->{bug_id};
+}
+
+=over
+
+=item C<description>
+
+user-provided text describing the attachment
+
+=back
+
+=cut
+
+sub description {
+    my $self = shift;
+    return $self->{description};
+}
+
+=over
+
+=item C<contenttype>
+
+the attachment's MIME media type
+
+=back
+
+=cut
+
+sub contenttype {
+    my $self = shift;
+    return $self->{contenttype};
+}
+
+=over
+
+=item C<attacher>
+
+the user who attached the attachment
+
+=back
+
+=cut
+
+sub attacher {
+    my $self = shift;
+    return $self->{attacher} if exists $self->{attacher};
+    $self->{attacher} = new Bugzilla::User($self->{attacher_id});
+    return $self->{attacher};
+}
+
+=over
+
+=item C<attached>
+
+the date and time on which the attacher attached the attachment
+
+=back
+
+=cut
+
+sub attached {
+    my $self = shift;
+    return $self->{attached};
+}
+
+=over
+
+=item C<modification_time>
+
+the date and time on which the attachment was last modified.
+
+=back
+
+=cut
+
+sub modification_time {
+    my $self = shift;
+    return $self->{modification_time};
+}
+
+=over
+
+=item C<filename>
+
+the name of the file the attacher attached
+
+=back
+
+=cut
+
+sub filename {
+    my $self = shift;
+    return $self->{filename};
+}
+
+=over
+
+=item C<ispatch>
+
+whether or not the attachment is a patch
+
+=back
+
+=cut
+
+sub ispatch {
+    my $self = shift;
+    return $self->{ispatch};
+}
+
+=over
+
+=item C<isurl>
+
+whether or not the attachment is a URL
+
+=back
+
+=cut
+
+sub isurl {
+    my $self = shift;
+    return $self->{isurl};
+}
+
+=over
+
+=item C<isobsolete>
+
+whether or not the attachment is obsolete
+
+=back
+
+=cut
+
+sub isobsolete {
+    my $self = shift;
+    return $self->{isobsolete};
+}
+
+=over
+
+=item C<isprivate>
+
+whether or not the attachment is private
+
+=back
+
+=cut
+
+sub isprivate {
+    my $self = shift;
+    return $self->{isprivate};
+}
+
+=over
+
+=item C<is_viewable>
+
+Returns 1 if the attachment has a content-type viewable in this browser.
+Note that we don't use $cgi->Accept()'s ability to check if a content-type
+matches, because this will return a value even if it's matched by the generic
+*/* which most browsers add to the end of their Accept: headers.
+
+=back
+
+=cut
+
+sub is_viewable {
+    my $self = shift;
+    my $contenttype = $self->contenttype;
+    my $cgi = Bugzilla->cgi;
+
+    # We assume we can view all text and image types.
+    return 1 if ($contenttype =~ /^(text|image)\//);
+
+    # Mozilla can view XUL. Note the trailing slash on the Gecko detection to
+    # avoid sending XUL to Safari.
+    return 1 if (($contenttype =~ /^application\/vnd\.mozilla\./)
+                 && ($cgi->user_agent() =~ /Gecko\//));
+
+    # If it's not one of the above types, we check the Accept: header for any
+    # types mentioned explicitly.
+    my $accept = join(",", $cgi->Accept());
+    return 1 if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/);
+
+    return 0;
+}
+
+=over
+
+=item C<data>
+
+the content of the attachment
+
+=back
+
+=cut
+
+sub data {
+    my $self = shift;
+    return $self->{data} if exists $self->{data};
+
+    # First try to get the attachment data from the database.
+    ($self->{data}) = Bugzilla->dbh->selectrow_array("SELECT thedata
+                                                      FROM attach_data
+                                                      WHERE id = ?",
+                                                     undef,
+                                                     $self->{id});
+
+    # If there's no attachment data in the database, the attachment is stored
+    # in a local file, so retrieve it from there.
+    if (length($self->{data}) == 0) {
+        if (open(AH, $self->_get_local_filename())) {
+            local $/;
+            binmode AH;
+            $self->{data} = <AH>;
+            close(AH);
+        }
+    }
+
+    return $self->{data};
+}
+
+=over
+
+=item C<datasize>
+
+the length (in characters) of the attachment content
+
+=back
+
+=cut
+
+# datasize is a property of the data itself, and it's unclear whether we should
+# expose it at all, since you can easily derive it from the data itself: in TT,
+# attachment.data.size; in Perl, length($attachment->{data}).  But perhaps
+# it makes sense for performance reasons, since accessing the data forces it
+# to get retrieved from the database/filesystem and loaded into memory,
+# while datasize avoids loading the attachment into memory, calling SQL's
+# LENGTH() function or stat()ing the file instead.  I've left it in for now.
+
+sub datasize {
+    my $self = shift;
+    return $self->{datasize} if exists $self->{datasize};
+
+    # If we have already retrieved the data, return its size.
+    return length($self->{data}) if exists $self->{data};
+
+    $self->{datasize} =
+        Bugzilla->dbh->selectrow_array("SELECT LENGTH(thedata)
+                                        FROM attach_data
+                                        WHERE id = ?",
+                                       undef, $self->{id}) || 0;
+
+    # If there's no attachment data in the database, either the attachment
+    # is stored in a local file, and so retrieve its size from the file,
+    # or the attachment has been deleted.
+    unless ($self->{datasize}) {
+        if (open(AH, $self->_get_local_filename())) {
+            binmode AH;
+            $self->{datasize} = (stat(AH))[7];
+            close(AH);
+        }
+    }
+
+    return $self->{datasize};
+}
+
+=over
+
+=item C<flags>
+
+flags that have been set on the attachment
+
+=back
+
+=cut
+
+sub flags {
+    my $self = shift;
+    return $self->{flags} if exists $self->{flags};
+
+    $self->{flags} = Bugzilla::Flag->match({ 'attach_id' => $self->id });
+    return $self->{flags};
+}
+
+# Instance methods; no POD documentation here yet because the only ones so far
+# are private.
+
+sub _get_local_filename {
+    my $self = shift;
+    my $hash = ($self->id % 100) + 100;
+    $hash =~ s/.*(\d\d)$/group.$1/;
+    return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id;
+}
+
+sub _validate_filename {
+    my ($throw_error) = @_;
+    my $cgi = Bugzilla->cgi;
+    defined $cgi->upload('data')
+        || ($throw_error ? ThrowUserError("file_not_specified") : return 0);
+
+    my $filename = $cgi->upload('data');
+
+    # Remove path info (if any) from the file name.  The browser should do this
+    # for us, but some are buggy.  This may not work on Mac file names and could
+    # mess up file names with slashes in them, but them's the breaks.  We only
+    # use this as a hint to users downloading attachments anyway, so it's not
+    # a big deal if it munges incorrectly occasionally.
+    $filename =~ s/^.*[\/\\]//;
+
+    # Truncate the filename to 100 characters, counting from the end of the
+    # string to make sure we keep the filename extension.
+    $filename = substr($filename, -100, 100);
+
+    return $filename;
+}
+
+sub _validate_data {
+    my ($throw_error, $hr_vars) = @_;
+    my $cgi = Bugzilla->cgi;
+    my $maxsize = $cgi->param('ispatch') ? Bugzilla->params->{'maxpatchsize'} 
+                  : Bugzilla->params->{'maxattachmentsize'};
+    $maxsize *= 1024; # Convert from K
+    my $fh;
+    # Skip uploading into a local variable if the user wants to upload huge
+    # attachments into local files.
+    if (!$cgi->param('bigfile')) {
+        $fh = $cgi->upload('data');
+    }
+    my $data;
+
+    # We could get away with reading only as much as required, except that then
+    # we wouldn't have a size to print to the error handler below.
+    if (!$cgi->param('bigfile')) {
+        # enable 'slurp' mode
+        local $/;
+        $data = <$fh>;
+    }
+
+    $data
+        || ($cgi->param('bigfile'))
+        || ($throw_error ? ThrowUserError("zero_length_file") : return 0);
+
+    # Windows screenshots are usually uncompressed BMP files which
+    # makes for a quick way to eat up disk space. Let's compress them.
+    # We do this before we check the size since the uncompressed version
+    # could easily be greater than maxattachmentsize.
+    if (Bugzilla->params->{'convert_uncompressed_images'}
+        && $cgi->param('contenttype') eq 'image/bmp') {
+        require Image::Magick;
+        my $img = Image::Magick->new(magick=>'bmp');
+        $img->BlobToImage($data);
+        $img->set(magick=>'png');
+        my $imgdata = $img->ImageToBlob();
+        $data = $imgdata;
+        $cgi->param('contenttype', 'image/png');
+        $hr_vars->{'convertedbmp'} = 1;
+    }
+
+    # Make sure the attachment does not exceed the maximum permitted size
+    my $len = $data ? length($data) : 0;
+    if ($maxsize && $len > $maxsize) {
+        my $vars = { filesize => sprintf("%.0f", $len/1024) };
+        if ($cgi->param('ispatch')) {
+            $throw_error ? ThrowUserError("patch_too_large", $vars) : return 0;
+        }
+        else {
+            $throw_error ? ThrowUserError("file_too_large", $vars) : return 0;
+        }
+    }
+
+    return $data || '';
+}
+
+=pod
+
+=head2 Class Methods
+
+=over
+
+=item C<get_attachments_by_bug($bug_id)>
+
+Description: retrieves and returns the attachments the currently logged in
+             user can view for the given bug.
+
+Params:     C<$bug_id> - integer - the ID of the bug for which
+            to retrieve and return attachments.
+
+Returns:    a reference to an array of attachment objects.
+
+=cut
+
+sub get_attachments_by_bug {
+    my ($class, $bug_id) = @_;
+    my $user = Bugzilla->user;
+    my $dbh = Bugzilla->dbh;
+
+    # By default, private attachments are not accessible, unless the user
+    # is in the insider group or submitted the attachment.
+    my $and_restriction = '';
+    my @values = ($bug_id);
+
+    unless ($user->is_insider) {
+        $and_restriction = 'AND (isprivate = 0 OR submitter_id = ?)';
+        push(@values, $user->id);
+    }
+
+    my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
+                                               WHERE bug_id = ? $and_restriction",
+                                               undef, @values);
+    my $attachments = Bugzilla::Attachment->get_list($attach_ids);
+    return $attachments;
+}
+
+=pod
+
+=item C<validate_is_patch()>
+
+Description: validates the "patch" flag passed in by CGI.
+
+Returns:    1 on success.
+
+=cut
+
+sub validate_is_patch {
+    my ($class, $throw_error) = @_;
+    my $cgi = Bugzilla->cgi;
+
+    # 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.
+    $cgi->param('ispatch', $cgi->param('ispatch') ? 1 : 0);
+
+    # Set the content type to text/plain if the attachment is a patch.
+    $cgi->param('contenttype', 'text/plain') if $cgi->param('ispatch');
+
+    return 1;
+}
+
+=pod
+
+=item C<validate_description()>
+
+Description: validates the description passed in by CGI.
+
+Returns:    1 on success.
+
+=cut
+
+sub validate_description {
+    my ($class, $throw_error) = @_;
+    my $cgi = Bugzilla->cgi;
+
+    $cgi->param('description')
+        || ($throw_error ? ThrowUserError("missing_attachment_description") : return 0);
+
+    return 1;
+}
+
+=pod
+
+=item C<validate_content_type()>
+
+Description: validates the content type passed in by CGI.
+
+Returns:    1 on success.
+
+=cut
+
+sub validate_content_type {
+    my ($class, $throw_error) = @_;
+    my $cgi = Bugzilla->cgi;
+
+    if (!defined $cgi->param('contenttypemethod')) {
+        $throw_error ? ThrowUserError("missing_content_type_method") : return 0;
+    }
+    elsif ($cgi->param('contenttypemethod') eq 'autodetect') {
+        my $contenttype =
+            $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'};
+        # The user asked us to auto-detect the content type, so use the type
+        # specified in the HTTP request headers.
+        if ( !$contenttype ) {
+            $throw_error ? ThrowUserError("missing_content_type") : return 0;
+        }
+        $cgi->param('contenttype', $contenttype);
+    }
+    elsif ($cgi->param('contenttypemethod') eq 'list') {
+        # The user selected a content type from the list, so use their
+        # selection.
+        $cgi->param('contenttype', $cgi->param('contenttypeselection'));
+    }
+    elsif ($cgi->param('contenttypemethod') eq 'manual') {
+        # The user entered a content type manually, so use their entry.
+        $cgi->param('contenttype', $cgi->param('contenttypeentry'));
+    }
+    else {
+        $throw_error ?
+            ThrowCodeError("illegal_content_type_method",
+                           { contenttypemethod => $cgi->param('contenttypemethod') }) :
+            return 0;
+    }
+
+    if ( $cgi->param('contenttype') !~
+           /^(application|audio|image|message|model|multipart|text|video)\/.+$/ ) {
+        $throw_error ?
+            ThrowUserError("invalid_content_type",
+                           { contenttype => $cgi->param('contenttype') }) :
+            return 0;
+    }
+
+    return 1;
+}
+
+=pod
+
+=item C<validate_can_edit($attachment, $product_id)>
+
+Description: validates if the user is allowed to view and edit the attachment.
+             Only the submitter or someone with editbugs privs can edit it.
+             Only the submitter and users in the insider group can view
+             private attachments.
+
+Params:      $attachment - the attachment object being edited.
+             $product_id - the product ID the attachment belongs to.
+
+Returns:     1 on success. Else an error is thrown.
+
+=cut
+
+sub validate_can_edit {
+    my ($attachment, $product_id) = @_;
+    my $user = Bugzilla->user;
+
+    # The submitter can edit their attachments.
+    return 1 if ($attachment->attacher->id == $user->id
+                 || ((!$attachment->isprivate || $user->is_insider)
+                      && $user->in_group('editbugs', $product_id)));
+
+    # If we come here, then this attachment cannot be seen by the user.
+    ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id });
+}
+
+=item C<validate_obsolete($bug)>
+
+Description: validates if attachments the user wants to mark as obsolete
+             really belong to the given bug and are not already obsolete.
+             Moreover, a user cannot mark an attachment as obsolete if
+             he cannot view it (due to restrictions on it).
+
+Params:      $bug - The bug object obsolete attachments should belong to.
+
+Returns:     1 on success. Else an error is thrown.
+
+=cut
+
+sub validate_obsolete {
+    my ($class, $bug) = @_;
+    my $cgi = Bugzilla->cgi;
+
+    # Make sure the attachment id is valid and the user has permissions to view
+    # the bug to which it is attached. Make sure also that the user can view
+    # the attachment itself.
+    my @obsolete_attachments;
+    foreach my $attachid ($cgi->param('obsolete')) {
+        my $vars = {};
+        $vars->{'attach_id'} = $attachid;
+
+        detaint_natural($attachid)
+          || ThrowCodeError('invalid_attach_id_to_obsolete', $vars);
+
+        my $attachment = Bugzilla::Attachment->get($attachid);
+
+        # Make sure the attachment exists in the database.
+        ThrowUserError('invalid_attach_id', $vars) unless $attachment;
+
+        # Check that the user can view and edit this attachment.
+        $attachment->validate_can_edit($bug->product_id);
+
+        $vars->{'description'} = $attachment->description;
+
+        if ($attachment->bug_id != $bug->bug_id) {
+            $vars->{'my_bug_id'} = $bug->bug_id;
+            $vars->{'attach_bug_id'} = $attachment->bug_id;
+            ThrowCodeError('mismatched_bug_ids_on_obsolete', $vars);
+        }
+
+        if ($attachment->isobsolete) {
+          ThrowCodeError('attachment_already_obsolete', $vars);
+        }
+
+        push(@obsolete_attachments, $attachment);
+    }
+    return @obsolete_attachments;
+}
+
+
+=pod
+
+=item C<insert_attachment_for_bug($throw_error, $bug, $user, $timestamp, $hr_vars)>
+
+Description: inserts an attachment from CGI input for the given bug.
+
+Params:     C<$bug> - Bugzilla::Bug object - the bug for which to insert
+            the attachment.
+            C<$user> - Bugzilla::User object - the user we're inserting an
+            attachment for.
+            C<$timestamp> - scalar - timestamp of the insert as returned
+            by SELECT NOW().
+            C<$hr_vars> - hash reference - reference to a hash of template
+            variables.
+
+Returns:    the ID of the new attachment.
+
+=cut
+
+sub insert_attachment_for_bug {
+    my ($class, $throw_error, $bug, $user, $timestamp, $hr_vars) = @_;
+
+    my $cgi = Bugzilla->cgi;
+    my $dbh = Bugzilla->dbh;
+    my $attachurl = $cgi->param('attachurl') || '';
+    my $data;
+    my $filename;
+    my $contenttype;
+    my $isurl;
+    $class->validate_is_patch($throw_error) || return;
+    $class->validate_description($throw_error) || return;
+
+    if (Bugzilla->params->{'allow_attach_url'}
+        && ($attachurl =~ /^(http|https|ftp):\/\/\S+/)
+        && !defined $cgi->upload('data'))
+    {
+        $filename = '';
+        $data = $attachurl;
+        $isurl = 1;
+        $contenttype = 'text/plain';
+        $cgi->param('ispatch', 0);
+        $cgi->delete('bigfile');
+    }
+    else {
+        $filename = _validate_filename($throw_error) || return;
+        # need to validate content type before data as
+        # we now check the content type for image/bmp in _validate_data()
+        unless ($cgi->param('ispatch')) {
+            $class->validate_content_type($throw_error) || return;
+
+            # Set the ispatch flag to 1 if we're set to autodetect
+            # and the content type is text/x-diff or text/x-patch
+            if ($cgi->param('contenttypemethod') eq 'autodetect'
+                && $cgi->param('contenttype') =~ m{text/x-(?:diff|patch)})
+            {
+                $cgi->param('ispatch', 1);
+                $cgi->param('contenttype', 'text/plain');
+            }
+        }
+        $data = _validate_data($throw_error, $hr_vars);
+        # If the attachment is stored locally, $data eq ''.
+        # If an error is thrown, $data eq '0'.
+        ($data ne '0') || return;
+        $contenttype = $cgi->param('contenttype');
+
+        # These are inserted using placeholders so no need to panic
+        trick_taint($filename);
+        trick_taint($contenttype);
+        $isurl = 0;
+    }
+
+    # Check attachments the user tries to mark as obsolete.
+    my @obsolete_attachments;
+    if ($cgi->param('obsolete')) {
+        @obsolete_attachments = $class->validate_obsolete($bug);
+    }
+
+    # The order of these function calls is important, as Flag::validate
+    # assumes User::match_field has ensured that the
+    # values in the requestee fields are legitimate user email addresses.
+    my $match_status = Bugzilla::User::match_field($cgi, {
+        '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
+    }, MATCH_SKIP_CONFIRM);
+
+    $hr_vars->{'match_field'} = 'requestee';
+    if ($match_status == USER_MATCH_FAILED) {
+        $hr_vars->{'message'} = 'user_match_failed';
+    }
+    elsif ($match_status == USER_MATCH_MULTIPLE) {
+        $hr_vars->{'message'} = 'user_match_multiple';
+    }
+
+    # Escape characters in strings that will be used in SQL statements.
+    my $description = $cgi->param('description');
+    trick_taint($description);
+    my $isprivate = $cgi->param('isprivate') ? 1 : 0;
+
+    # Insert the attachment into the database.
+    my $sth = $dbh->do(
+        "INSERT INTO attachments
+            (bug_id, creation_ts, modification_time, filename, description,
+             mimetype, ispatch, isurl, isprivate, submitter_id)
+         VALUES (?,?,?,?,?,?,?,?,?,?)", undef, ($bug->bug_id, $timestamp, $timestamp,
+              $filename, $description, $contenttype, $cgi->param('ispatch'),
+              $isurl, $isprivate, $user->id));
+    # Retrieve the ID of the newly created attachment record.
+    my $attachid = $dbh->bz_last_key('attachments', 'attach_id');
+
+    # We only use $data here in this INSERT with a placeholder,
+    # so it's safe.
+    $sth = $dbh->prepare("INSERT INTO attach_data
+                         (id, thedata) VALUES ($attachid, ?)");
+    trick_taint($data);
+    $sth->bind_param(1, $data, $dbh->BLOB_TYPE);
+    $sth->execute();
+
+    # If the file is to be stored locally, stream the file from the web server
+    # to the local file without reading it into a local variable.
+    if ($cgi->param('bigfile')) {
+        my $attachdir = bz_locations()->{'attachdir'};
+        my $fh = $cgi->upload('data');
+        my $hash = ($attachid % 100) + 100;
+        $hash =~ s/.*(\d\d)$/group.$1/;
+        mkdir "$attachdir/$hash", 0770;
+        chmod 0770, "$attachdir/$hash";
+        open(AH, ">$attachdir/$hash/attachment.$attachid");
+        binmode AH;
+        my $sizecount = 0;
+        my $limit = (Bugzilla->params->{"maxlocalattachment"} * 1048576);
+        while (<$fh>) {
+            print AH $_;
+            $sizecount += length($_);
+            if ($sizecount > $limit) {
+                close AH;
+                close $fh;
+                unlink "$attachdir/$hash/attachment.$attachid";
+                $throw_error ? ThrowUserError("local_file_too_large") : return;
+            }
+        }
+        close AH;
+        close $fh;
+    }
+
+    # Make existing attachments obsolete.
+    my $fieldid = get_field_id('attachments.isobsolete');
+
+    foreach my $obsolete_attachment (@obsolete_attachments) {
+        # If the obsolete attachment has request flags, cancel them.
+        # This call must be done before updating the 'attachments' table.
+        Bugzilla::Flag->CancelRequests($bug, $obsolete_attachment, $timestamp);
+
+        $dbh->do('UPDATE attachments SET isobsolete = 1, modification_time = ?
+                  WHERE attach_id = ?',
+                 undef, ($timestamp, $obsolete_attachment->id));
+
+        $dbh->do('INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
+                                             fieldid, removed, added)
+                       VALUES (?,?,?,?,?,?,?)',
+                  undef, ($bug->bug_id, $obsolete_attachment->id, $user->id,
+                          $timestamp, $fieldid, 0, 1));
+    }
+
+    my $attachment = Bugzilla::Attachment->get($attachid);
+
+    # 1. Add flags, if any. To avoid dying if something goes wrong
+    # while processing flags, we will eval() flag validation.
+    # This requires errors to die().
+    # XXX: this can go away as soon as flag validation is able to
+    #      fail without dying.
+    #
+    # 2. Flag::validate() should not detect any reference to existing flags
+    # when creating a new attachment. Setting the third param to -1 will
+    # force this function to check this point.
+    my $error_mode_cache = Bugzilla->error_mode;
+    Bugzilla->error_mode(ERROR_MODE_DIE);
+    eval {
+        Bugzilla::Flag::validate($bug->bug_id, -1, SKIP_REQUESTEE_ON_ERROR);
+        Bugzilla::Flag->process($bug, $attachment, $timestamp, $hr_vars);
+    };
+    Bugzilla->error_mode($error_mode_cache);
+    if ($@) {
+        $hr_vars->{'message'} = 'flag_creation_failed';
+        $hr_vars->{'flag_creation_error'} = $@;
+    }
+
+    # Return the new attachment object.
+    return $attachment;
+}
+
+=pod
+
+=item C<remove_from_db()>
+
+Description: removes an attachment from the DB.
+
+Params:     none
+
+Returns:    nothing
+
+=back
+
+=cut
+
+sub remove_from_db {
+    my $self = shift;
+    my $dbh = Bugzilla->dbh;
+
+    $dbh->bz_start_transaction();
+    $dbh->do('DELETE FROM flags WHERE attach_id = ?', undef, $self->id);
+    $dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id);
+    $dbh->do('UPDATE attachments SET mimetype = ?, ispatch = ?, isurl = ?, isobsolete = ?
+              WHERE attach_id = ?', undef, ('text/plain', 0, 0, 1, $self->id));
+    $dbh->bz_commit_transaction();
+}
+
+1;