From e47e36a6821a4f954f5d03643cdb5be988568798 Mon Sep 17 00:00:00 2001
From: "bbaetz%student.usyd.edu.au" <>
Date: Thu, 28 Nov 2002 18:49:38 +0000
Subject: [PATCH] Bug 171493 - make show_bug use Bug.pm and remove bug_form.pl
 r=justdave, joel a=justdave

---
 Bug.pm                                        | 502 ++++++++++--------
 Bugzilla/Bug.pm                               | 502 ++++++++++--------
 Bugzilla/User.pm                              |   2 +-
 bug_form.pl                                   | 398 --------------
 checksetup.pl                                 |  10 +-
 globals.pl                                    |  19 +-
 post_bug.cgi                                  |  45 +-
 process_bug.cgi                               |  48 +-
 show_bug.cgi                                  |  38 +-
 t/004template.t                               |   2 +
 template/en/default/bug/comments.html.tmpl    |   2 +-
 .../en/default/bug/create/created.html.tmpl   |  20 +-
 template/en/default/bug/edit.html.tmpl        | 101 ++--
 .../en/default/bug/process/next.html.tmpl     |  13 +-
 template/en/default/bug/show.html.tmpl        |  46 ++
 .../en/default/global/code-error.html.tmpl    |   4 +
 .../default/global/site-navigation.html.tmpl  |   4 +-
 template/en/default/pages/linked.html.tmpl    |   4 +-
 18 files changed, 808 insertions(+), 952 deletions(-)
 delete mode 100644 bug_form.pl
 create mode 100644 template/en/default/bug/show.html.tmpl

diff --git a/Bug.pm b/Bug.pm
index 11eb43af1..91c20780a 100755
--- a/Bug.pm
+++ b/Bug.pm
@@ -21,26 +21,41 @@
 #                 Terry Weissman <terry@mozilla.org>
 #                 Chris Yeh      <cyeh@bluemartini.com>
 
+package Bug;
+
 use strict;
 
-use DBI;
 use RelationSet;
-use vars qw($unconfirmedstate $legal_keywords);
+use vars qw($unconfirmedstate $legal_keywords @legal_platform
+            @legal_priority @legal_severity @legal_opsys @legal_bugs_status
+            @settable_resolution %components %versions %target_milestone
+            @enterable_products %milestoneurl %prodmaxvotes);
 
-package Bug;
 use CGI::Carp qw(fatalsToBrowser);
 my %ok_field;
 
+use Attachment;
 use Bugzilla::Config;
+use Bugzilla::Constants;
+use Bugzilla::Flag;
+use Bugzilla::FlagType;
+use Bugzilla::User;
 use Bugzilla::Util;
 
 for my $key (qw (bug_id alias product version rep_platform op_sys bug_status 
-                resolution priority bug_severity component assigned_to
-                reporter bug_file_loc short_desc target_milestone 
-                qa_contact status_whiteboard creation_ts 
-                delta_ts votes whoid comment query error) ){
+                 resolution priority bug_severity component assigned_to
+                 reporter bug_file_loc short_desc target_milestone 
+                 qa_contact status_whiteboard creation_ts keywords
+                 delta_ts votes whoid usergroupset comment query error
+                 longdescs cc milestoneurl attachments dependson blocked
+                 cclist_accessible reporter_accessible
+                 isopened isunconfirmed assigned_to_name assigned_to_email
+                 qa_contact_name qa_contact_email reporter_name
+                 reporter_email flag_types num_attachment_flag_types
+                 show_attachment_flags use_keywords any_flags_requesteeble
+                 estimated_time remaining_time actual_time) ) {
     $ok_field{$key}++;
-    }
+}
 
 # create a new empty bug
 #
@@ -95,16 +110,20 @@ sub initBug  {
         $user_id = &::DBname_to_id($user_id); 
      }
   }
-     
+
   $self->{'whoid'} = $user_id;
 
   my $query = "
-    select
-      bugs.bug_id, alias, products.name, version, rep_platform, op_sys, bug_status,
-      resolution, priority, bug_severity, components.name, assigned_to, reporter,
-      bug_file_loc, short_desc, target_milestone, qa_contact,
-      status_whiteboard, date_format(creation_ts,'%Y-%m-%d %H:%i'),
-      delta_ts, sum(votes.count)
+    SELECT
+      bugs.bug_id, alias, bugs.product_id, products.name, version,
+      rep_platform, op_sys, bug_status, resolution, priority,
+      bug_severity, bugs.component_id, components.name, assigned_to,
+      reporter, bug_file_loc, short_desc, target_milestone,
+      qa_contact, status_whiteboard,
+      DATE_FORMAT(creation_ts,'%Y.%m.%d %H:%i'),
+      delta_ts, sum(votes.count),
+      reporter_accessible, cclist_accessible,
+      estimated_time, remaining_time
     from bugs left join votes using(bug_id),
       products, components
     where bugs.bug_id = $bug_id
@@ -118,14 +137,17 @@ sub initBug  {
   if ((@row = &::FetchSQLData()) && &::CanSeeBug($bug_id, $self->{'whoid'})) {
     my $count = 0;
     my %fields;
-    foreach my $field ("bug_id", "alias", "product", "version", "rep_platform",
-                       "op_sys", "bug_status", "resolution", "priority",
-                       "bug_severity", "component", "assigned_to", "reporter",
-                       "bug_file_loc", "short_desc", "target_milestone",
-                       "qa_contact", "status_whiteboard", "creation_ts",
-                       "delta_ts", "votes") {
+    foreach my $field ("bug_id", "alias", "product_id", "product", "version", 
+                       "rep_platform", "op_sys", "bug_status", "resolution", 
+                       "priority", "bug_severity", "component_id", "component",
+                       "assigned_to", "reporter", "bug_file_loc", "short_desc",
+                       "target_milestone", "qa_contact", "status_whiteboard", 
+                       "creation_ts", "delta_ts", "votes",
+                       "reporter_accessible", "cclist_accessible".
+                       "estimated_time", "remaining_time")
+      {
         $fields{$field} = shift @row;
-        if ($fields{$field}) {
+        if (defined $fields{$field}) {
             $self->{$field} = $fields{$field};
         }
         $count++;
@@ -140,8 +162,12 @@ sub initBug  {
       return $self;
   }
 
-  $self->{'assigned_to'} = &::DBID_to_name($self->{'assigned_to'});
-  $self->{'reporter'} = &::DBID_to_name($self->{'reporter'});
+  $self->{'assigned_to'} = new Bugzilla::User($self->{'assigned_to'});
+  $self->{'reporter'} = new Bugzilla::User($self->{'reporter'});
+
+  if (Param('useqacontact') && $self->{'qa_contact'} > 0) {
+      $self->{'qa_contact'} = new Bugzilla::User($self->{'qa_contact'});
+  }
 
   my $ccSet = new RelationSet;
   $ccSet->mergeFromDB("select who from cc where bug_id=$bug_id");
@@ -150,13 +176,6 @@ sub initBug  {
     $self->{'cc'} = \@cc;
   }
 
-  if (Param("useqacontact") && (defined $self->{'qa_contact'}) ) {
-    my $name = $self->{'qa_contact'} > 0 ? &::DBID_to_name($self->{'qa_contact'}) :"";
-    if ($name) {
-      $self->{'qa_contact'} = $name;
-    }
-  }
-
   if (@::legal_keywords) {
     &::SendSQL("SELECT keyworddefs.name 
               FROM keyworddefs, keywords
@@ -172,39 +191,43 @@ sub initBug  {
     }
   }
 
-    &::SendSQL("select attach_id, creation_ts, isprivate, description 
-             from attachments 
-             where bug_id = $bug_id");
-    my @attachments;
-    while (&::MoreSQLData()) {
-        my ($attachid, $date, $isprivate, $desc) = (&::FetchSQLData());
-        my %attach;
-        $attach{'attachid'} = $attachid;
-        $attach{'isprivate'} = $isprivate;
-        $attach{'date'} = $date;
-        $attach{'desc'} = $desc;
-        push @attachments, \%attach;
-    }
-    if (@attachments) {
-        $self->{'attachments'} = \@attachments;
-    }
-
-    &::SendSQL("select bug_id, who, bug_when, isprivate, thetext 
-           from longdescs 
-           where bug_id = $bug_id");
-    my @longdescs;
-    while (&::MoreSQLData()) {
-        my ($bug_id, $who, $bug_when, $isprivate, $thetext) = (&::FetchSQLData());
-        my %longdesc;
-        $longdesc{'who'} = $who;
-        $longdesc{'bug_when'} = $bug_when;
-        $longdesc{'isprivate'} = $isprivate;
-        $longdesc{'thetext'} = $thetext;
-        push @longdescs, \%longdesc;
-    }
-    if (@longdescs) {
-        $self->{'longdescs'} = \@longdescs;
-    }
+  $self->{'attachments'} = Attachment::query($self->{bug_id});
+
+  # The types of flags that can be set on this bug.
+  # If none, no UI for setting flags will be displayed.
+  my $flag_types = 
+    Bugzilla::FlagType::match({ 'target_type'  => 'bug', 
+                                'product_id'   => $self->{'product_id'}, 
+                                'component_id' => $self->{'component_id'} });
+  foreach my $flag_type (@$flag_types) {
+      $flag_type->{'flags'} = 
+        Bugzilla::Flag::match({ 'bug_id'      => $self->{bug_id},
+                                'type_id'     => $flag_type->{'id'},
+                                'target_type' => 'bug' });
+  }
+  $self->{'flag_types'} = $flag_types;
+  $self->{'any_flags_requesteeable'} = grep($_->{'is_requesteeble'}, @$flag_types);
+
+  # The number of types of flags that can be set on attachments to this bug
+  # and the number of flags on those attachments.  One of these counts must be
+  # greater than zero in order for the "flags" column to appear in the table
+  # of attachments.
+  my $num_attachment_flag_types =
+    Bugzilla::FlagType::count({ 'target_type'  => 'attachment',
+                                'product_id'   => $self->{'product_id'},
+                                'component_id' => $self->{'component_id'},
+                                'is_active'    => 1 });
+  my $num_attachment_flags =
+    Bugzilla::Flag::count({ 'target_type'  => 'attachment',
+                            'bug_id'       => $self->{bug_id} });
+
+  $self->{'show_attachment_flags'}
+    = $num_attachment_flag_types || $num_attachment_flags;
+
+  $self->{'milestoneurl'} = $::milestoneurl{$self->{product}};
+
+  $self->{'isunconfirmed'} = ($self->{bug_status} eq $::unconfirmedstate);
+  $self->{'isopened'} = &::IsOpenedState($self->{bug_status});
   
   my @depends = EmitDependList("blocked", "dependson", $bug_id);
   if (@depends) {
@@ -218,7 +241,191 @@ sub initBug  {
   return $self;
 }
 
+sub actual_time {
+    my ($self) = @_;
+
+    return $self->{'actual_time'} if exists $self->{'actual_time'};
+
+    if (&::UserInGroup(Param("timetrackinggroup"))) {
+        &::SendSQL("SELECT SUM(work_time)
+               FROM longdescs WHERE longdescs.bug_id=$self->{bug_id}");
+        $self->{'actual_time'} = &::FetchSQLData();
+    }
+
+    return $self->{'actual_time'};
+}
+
+sub longdescs {
+    my ($self) = @_;
+
+    return $self->{'longdescs'} if exists $self->{'longdescs'};
+
+    $self->{'longdescs'} = &::GetComments($self->{bug_id});
+
+    return $self->{'longdescs'};
+}
+
+sub use_keywords {
+    return @::legal_keywords;
+}
+
+sub use_votes {
+    my ($self) = @_;
+
+    return Param('usevotes')
+      && $::prodmaxvotes{$self->{product}} > 0;
+}
+
+sub groups {
+    my $self = shift;
+
+    return $self->{'groups'} if exists $self->{'groups'};
+
+    my @groups;
+
+    # Some of this stuff needs to go into Bugzilla::User
+
+    # For every group, we need to know if there is ANY bug_group_map
+    # record putting the current bug in that group and if there is ANY
+    # user_group_map record putting the user in that group.
+    # The LEFT JOINs are checking for record existence.
+    #
+    &::SendSQL("SELECT DISTINCT groups.id, name, description," .
+             " bug_group_map.group_id IS NOT NULL," .
+             " user_group_map.group_id IS NOT NULL," .
+             " isactive, membercontrol, othercontrol" .
+             " FROM groups" . 
+             " LEFT JOIN bug_group_map" .
+             " ON bug_group_map.group_id = groups.id" .
+             " AND bug_id = $self->{'bug_id'}" .
+             " LEFT JOIN user_group_map" .
+             " ON user_group_map.group_id = groups.id" .
+             " AND user_id = $::userid" .
+             " AND NOT isbless" .
+             " LEFT JOIN group_control_map" .
+             " ON group_control_map.group_id = groups.id" .
+             " AND group_control_map.product_id = " . $self->{'product_id'} .
+             " WHERE isbuggroup");
+
+    while (&::MoreSQLData()) {
+        my ($groupid, $name, $description, $ison, $ingroup, $isactive,
+            $membercontrol, $othercontrol) = &::FetchSQLData();
+
+        $membercontrol ||= 0;
+
+        # For product groups, we only want to use the group if either
+        # (1) The bit is set and not required, or
+        # (2) The group is Shown or Default for members and
+        #     the user is a member of the group.
+        if ($ison ||
+            ($isactive && $ingroup
+                       && (($membercontrol == CONTROLMAPDEFAULT)
+                           || ($membercontrol == CONTROLMAPSHOWN))
+            ))
+        {
+            my $ismandatory = $isactive
+              && ($membercontrol == CONTROLMAPMANDATORY);
+
+            push (@groups, { "bit" => $groupid,
+                             "ison" => $ison,
+                             "ingroup" => $ingroup,
+                             "mandatory" => $ismandatory,
+                             "description" => $description });
+        }
+    }
+
+    $self->{'groups'} = \@groups;
+
+    return $self->{'groups'};
+}
+
+sub user {
+    my $self = shift;
+    return $self->{'user'} if exists $self->{'user'};
+
+    $self->{'user'} = {};
+
+    my $movers = Param("movers");
+    $self->{'user'}->{'canmove'} = Param("move-enabled") 
+      && (defined $::COOKIE{"Bugzilla_login"}) 
+        && ($::COOKIE{"Bugzilla_login"} =~ /$movers/);
+
+    # In the below, if the person hasn't logged in ($::userid == 0), then
+    # we treat them as if they can do anything.  That's because we don't
+    # know why they haven't logged in; it may just be because they don't
+    # use cookies.  Display everything as if they have all the permissions
+    # in the world; their permissions will get checked when they log in
+    # and actually try to make the change.
+    $self->{'user'}->{'canedit'} = $::userid == 0
+                                   || $::userid == $self->{'reporter'}
+                                   || $::userid == $self->{'qa_contact'}
+                                   || $::userid == $self->{'assigned_to'}
+                                   || &::UserInGroup("editbugs");
+    $self->{'user'}->{'canconfirm'} = ($::userid == 0)
+                                   || &::UserInGroup("canconfirm")
+                                   || &::UserInGroup("editbugs");
+
+    return $self->{'user'};
+}
+
+sub choices {
+    my $self = shift;
+    return $self->{'choices'} if exists $self->{'choices'};
+
+    &::GetVersionTable();
+
+    $self->{'choices'} = {};
 
+    # Fiddle the product list.
+    my $seen_curr_prod;
+    my @prodlist;
+
+    foreach my $product (@::enterable_products) {
+        if ($product eq $self->{'product'}) {
+            # if it's the product the bug is already in, it's ALWAYS in
+            # the popup, period, whether the user can see it or not, and
+            # regardless of the disallownew setting.
+            $seen_curr_prod = 1;
+            push(@prodlist, $product);
+            next;
+        }
+
+        if (!&::CanEnterProduct($product)) {
+            # If we're using bug groups to restrict entry on products, and
+            # this product has an entry group, and the user is not in that
+            # group, we don't want to include that product in this list.
+            next;
+        }
+
+        push(@prodlist, $product);
+    }
+
+    # The current product is part of the popup, even if new bugs are no longer
+    # allowed for that product
+    if (!$seen_curr_prod) {
+        push (@prodlist, $self->{'product'});
+        @prodlist = sort @prodlist;
+    }
+
+    # Hack - this array contains "". See bug 106589.
+    my @res = grep ($_, @::settable_resolution);
+
+    $self->{'choices'} =
+      {
+       'product' => \@prodlist,
+       'rep_platform' => \@::legal_platform,
+       'priority' => \@::legal_priority,
+       'bug_severity' => \@::legal_severity,
+       'op_sys' => \@::legal_opsys,
+       'bug_status' => \@::legal_bugs_status,
+       'resolution' => \@res,
+       'component' => $::components{$self->{product}},
+       'version' => $::versions{$self->{product}},
+       'target_milestone' => $::target_milestone{$self->{product}},
+      };
+
+    return $self->{'choices'};
+}
 
 # given a bug hash, emit xml for it. with file header provided by caller
 #
@@ -261,11 +468,11 @@ sub emitXML {
                      && Param("insidergroup")
                      && !&::UserInGroup(Param("insidergroup")));
             $xml .= "  <long_desc>\n"; 
-            $xml .= "   <who>" . &::DBID_to_name($self->{'longdescs'}[$i]->{'who'}) 
+            $xml .= "   <who>" . $self->{'longdescs'}[$i]->{'email'} 
                                . "</who>\n"; 
-            $xml .= "   <bug_when>" . $self->{'longdescs'}[$i]->{'bug_when'} 
+            $xml .= "   <bug_when>" . $self->{'longdescs'}[$i]->{'time'} 
                                     . "</bug_when>\n"; 
-            $xml .= "   <thetext>" . QuoteXMLChars($self->{'longdescs'}[$i]->{'thetext'})
+            $xml .= "   <thetext>" . QuoteXMLChars($self->{'longdescs'}[$i]->{'body'})
                                    . "</thetext>\n"; 
             $xml .= "  </long_desc>\n"; 
         }
@@ -280,7 +487,7 @@ sub emitXML {
             $xml .= "    <attachid>" . $self->{'attachments'}[$i]->{'attachid'}
                                     . "</attachid>\n"; 
             $xml .= "    <date>" . $self->{'attachments'}[$i]->{'date'} . "</date>\n"; 
-            $xml .= "    <desc>" . QuoteXMLChars($self->{'attachments'}[$i]->{'desc'}) . "</desc>\n"; 
+            $xml .= "    <desc>" . QuoteXMLChars($self->{'attachments'}[$i]->{'description'}) . "</desc>\n"; 
           # $xml .= "    <type>" . $self->{'attachments'}[$i]->{'type'} . "</type>\n"; 
           # $xml .= "    <data>" . $self->{'attachments'}[$i]->{'data'} . "</data>\n"; 
             $xml .= "  </attachment>\n"; 
@@ -310,8 +517,8 @@ sub QuoteXMLChars {
   $_[0] =~ s/&/&amp;/g;
   $_[0] =~ s/</&lt;/g;
   $_[0] =~ s/>/&gt;/g;
-  $_[0] =~ s/'/&apos;/g;
-  $_[0] =~ s/"/&quot;/g;
+  $_[0] =~ s/\'/&apos;/g;
+  $_[0] =~ s/\"/&quot;/g;
 # $_[0] =~ s/([\x80-\xFF])/&XmlUtf8Encode(ord($1))/ge;
   return($_[0]);
 }
@@ -341,156 +548,25 @@ sub XML_Footer {
   return ("</bugzilla>\n");
 }
 
-sub CanChangeField {
-   my $self = shift();
-   my ($f, $oldvalue, $newvalue) = (@_);
-   my $UserInEditGroupSet = -1;
-   my $UserInCanConfirmGroupSet = -1;
-   my $ownerid;
-   my $reporterid;
-   my $qacontactid;
-
-    if ($f eq "assigned_to" || $f eq "reporter" || $f eq "qa_contact") {
-        if ($oldvalue =~ /^\d+$/) {
-            if ($oldvalue == 0) {
-                $oldvalue = "";
-            } else {
-                $oldvalue = &::DBID_to_name($oldvalue);
-            }
-        }
-    }
-    if ($oldvalue eq $newvalue) {
-        return 1;
-    }
-    if (trim($oldvalue) eq trim($newvalue)) {
-        return 1;
-    }
-    if ($f =~ /^longdesc/) {
-        return 1;
-    }
-    if ($UserInEditGroupSet < 0) {
-        $UserInEditGroupSet = UserInGroup($self, "editbugs");
-    }
-    if ($UserInEditGroupSet) {
-        return 1;
-    }
-    &::SendSQL("SELECT reporter, assigned_to, qa_contact FROM bugs " .
-                "WHERE bug_id = $self->{'bug_id'}");
-    ($reporterid, $ownerid, $qacontactid) = (&::FetchSQLData());
-
-    # Let reporter change bug status, even if they can't edit bugs.
-    # If reporter can't re-open their bug they will just file a duplicate.
-    # While we're at it, let them close their own bugs as well.
-    if ( ($f eq "bug_status") && ($self->{'whoid'} eq $reporterid) ) {
-        return 1;
-    }
-    if ($f eq "bug_status" && $newvalue ne $::unconfirmedstate &&
-        &::IsOpenedState($newvalue)) {
-
-        # Hmm.  They are trying to set this bug to some opened state
-        # that isn't the UNCONFIRMED state.  Are they in the right
-        # group?  Or, has it ever been confirmed?  If not, then this
-        # isn't legal.
-
-        if ($UserInCanConfirmGroupSet < 0) {
-            $UserInCanConfirmGroupSet = &::UserInGroup("canconfirm");
-        }
-        if ($UserInCanConfirmGroupSet) {
-            return 1;
-        }
-        &::SendSQL("SELECT everconfirmed FROM bugs WHERE bug_id = $self->{'bug_id'}");
-        my $everconfirmed = FetchOneColumn();
-        if ($everconfirmed) {
-            return 1;
-        }
-    } elsif ($reporterid eq $self->{'whoid'} || $ownerid eq $self->{'whoid'} ||
-             $qacontactid eq $self->{'whoid'}) {
-        return 1;
-    }
-    $self->{'error'} = "
-Only the owner or submitter of the bug, or a sufficiently
-empowered user, may make that change to the $f field."
-}
-
-sub Collision {
-    my $self = shift();
-    my $write = "WRITE";        # Might want to make a param to control
-                                # whether we do LOW_PRIORITY ...
-    &::SendSQL("LOCK TABLES bugs $write, bugs_activity $write, cc $write, " .
-               "cc AS selectVisible_cc $write, " .
-            "profiles $write, dependencies $write, votes $write, " .
-            "keywords $write, longdescs $write, fielddefs $write, " .
-            "keyworddefs READ, groups READ, attachments READ, products READ");
-    &::SendSQL("SELECT delta_ts FROM bugs where bug_id=$self->{'bug_id'}");
-    my $delta_ts = &::FetchOneColumn();
-    &::SendSQL("unlock tables");
-    if ($self->{'delta_ts'} ne $delta_ts) {
-       return 1;
-    }
-    else {
-       return 0;
-    }
-}
-
-sub AppendComment  {
-    my $self = shift();
-    my ($comment) = (@_);
-    $comment =~ s/\r\n/\n/g;     # Get rid of windows-style line endings.
-    $comment =~ s/\r/\n/g;       # Get rid of mac-style line endings.
-    if ($comment =~ /^\s*$/) {  # Nothin' but whitespace.
-        return;
-    }
-
-    &::SendSQL("INSERT INTO longdescs (bug_id, who, bug_when, thetext) " .
-            "VALUES($self->{'bug_id'}, $self->{'whoid'}, now(), " . &::SqlQuote($comment) . ")");
-
-    &::SendSQL("UPDATE bugs SET delta_ts = now() WHERE bug_id = $self->{'bug_id'}");
-}
-
-
-#from o'reilley's Programming Perl
-sub display {
-    my $self = shift;
-    my @keys;
-    if (@_ == 0) {                  # no further arguments
-        @keys = sort keys(%$self);
-    }  else {
-        @keys = @_;                 # use the ones given
-    }
-    foreach my $key (@keys) {
-        print "\t$key => $self->{$key}\n";
-    }
-}
-
-sub CommitChanges {
-
-#snapshot bug
-#snapshot dependencies
-#check can change fields
-#check collision
-#lock and change fields
-#notify through mail
-
-}
-
 sub AUTOLOAD {
   use vars qw($AUTOLOAD);
-  my $self = shift;
-  my $type = ref($self) || $self;
   my $attr = $AUTOLOAD;
 
   $attr =~ s/.*:://;
   return unless $attr=~ /[^A-Z]/;
-  if (@_) {
-    $self->{$attr} = shift;
-    return;
-  }
   confess ("invalid bug attribute $attr") unless $ok_field{$attr};
-  if (defined $self->{$attr}) {
-    return $self->{$attr};
-  } else {
-    return '';
-  }
+
+  no strict 'refs';
+  *$AUTOLOAD = sub {
+      my $self = shift;
+      if (defined $self->{$attr}) {
+          return $self->{$attr};
+      } else {
+          return '';
+      }
+  };
+
+  goto &$AUTOLOAD;
 }
 
 1;
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 11eb43af1..91c20780a 100755
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -21,26 +21,41 @@
 #                 Terry Weissman <terry@mozilla.org>
 #                 Chris Yeh      <cyeh@bluemartini.com>
 
+package Bug;
+
 use strict;
 
-use DBI;
 use RelationSet;
-use vars qw($unconfirmedstate $legal_keywords);
+use vars qw($unconfirmedstate $legal_keywords @legal_platform
+            @legal_priority @legal_severity @legal_opsys @legal_bugs_status
+            @settable_resolution %components %versions %target_milestone
+            @enterable_products %milestoneurl %prodmaxvotes);
 
-package Bug;
 use CGI::Carp qw(fatalsToBrowser);
 my %ok_field;
 
+use Attachment;
 use Bugzilla::Config;
+use Bugzilla::Constants;
+use Bugzilla::Flag;
+use Bugzilla::FlagType;
+use Bugzilla::User;
 use Bugzilla::Util;
 
 for my $key (qw (bug_id alias product version rep_platform op_sys bug_status 
-                resolution priority bug_severity component assigned_to
-                reporter bug_file_loc short_desc target_milestone 
-                qa_contact status_whiteboard creation_ts 
-                delta_ts votes whoid comment query error) ){
+                 resolution priority bug_severity component assigned_to
+                 reporter bug_file_loc short_desc target_milestone 
+                 qa_contact status_whiteboard creation_ts keywords
+                 delta_ts votes whoid usergroupset comment query error
+                 longdescs cc milestoneurl attachments dependson blocked
+                 cclist_accessible reporter_accessible
+                 isopened isunconfirmed assigned_to_name assigned_to_email
+                 qa_contact_name qa_contact_email reporter_name
+                 reporter_email flag_types num_attachment_flag_types
+                 show_attachment_flags use_keywords any_flags_requesteeble
+                 estimated_time remaining_time actual_time) ) {
     $ok_field{$key}++;
-    }
+}
 
 # create a new empty bug
 #
@@ -95,16 +110,20 @@ sub initBug  {
         $user_id = &::DBname_to_id($user_id); 
      }
   }
-     
+
   $self->{'whoid'} = $user_id;
 
   my $query = "
-    select
-      bugs.bug_id, alias, products.name, version, rep_platform, op_sys, bug_status,
-      resolution, priority, bug_severity, components.name, assigned_to, reporter,
-      bug_file_loc, short_desc, target_milestone, qa_contact,
-      status_whiteboard, date_format(creation_ts,'%Y-%m-%d %H:%i'),
-      delta_ts, sum(votes.count)
+    SELECT
+      bugs.bug_id, alias, bugs.product_id, products.name, version,
+      rep_platform, op_sys, bug_status, resolution, priority,
+      bug_severity, bugs.component_id, components.name, assigned_to,
+      reporter, bug_file_loc, short_desc, target_milestone,
+      qa_contact, status_whiteboard,
+      DATE_FORMAT(creation_ts,'%Y.%m.%d %H:%i'),
+      delta_ts, sum(votes.count),
+      reporter_accessible, cclist_accessible,
+      estimated_time, remaining_time
     from bugs left join votes using(bug_id),
       products, components
     where bugs.bug_id = $bug_id
@@ -118,14 +137,17 @@ sub initBug  {
   if ((@row = &::FetchSQLData()) && &::CanSeeBug($bug_id, $self->{'whoid'})) {
     my $count = 0;
     my %fields;
-    foreach my $field ("bug_id", "alias", "product", "version", "rep_platform",
-                       "op_sys", "bug_status", "resolution", "priority",
-                       "bug_severity", "component", "assigned_to", "reporter",
-                       "bug_file_loc", "short_desc", "target_milestone",
-                       "qa_contact", "status_whiteboard", "creation_ts",
-                       "delta_ts", "votes") {
+    foreach my $field ("bug_id", "alias", "product_id", "product", "version", 
+                       "rep_platform", "op_sys", "bug_status", "resolution", 
+                       "priority", "bug_severity", "component_id", "component",
+                       "assigned_to", "reporter", "bug_file_loc", "short_desc",
+                       "target_milestone", "qa_contact", "status_whiteboard", 
+                       "creation_ts", "delta_ts", "votes",
+                       "reporter_accessible", "cclist_accessible".
+                       "estimated_time", "remaining_time")
+      {
         $fields{$field} = shift @row;
-        if ($fields{$field}) {
+        if (defined $fields{$field}) {
             $self->{$field} = $fields{$field};
         }
         $count++;
@@ -140,8 +162,12 @@ sub initBug  {
       return $self;
   }
 
-  $self->{'assigned_to'} = &::DBID_to_name($self->{'assigned_to'});
-  $self->{'reporter'} = &::DBID_to_name($self->{'reporter'});
+  $self->{'assigned_to'} = new Bugzilla::User($self->{'assigned_to'});
+  $self->{'reporter'} = new Bugzilla::User($self->{'reporter'});
+
+  if (Param('useqacontact') && $self->{'qa_contact'} > 0) {
+      $self->{'qa_contact'} = new Bugzilla::User($self->{'qa_contact'});
+  }
 
   my $ccSet = new RelationSet;
   $ccSet->mergeFromDB("select who from cc where bug_id=$bug_id");
@@ -150,13 +176,6 @@ sub initBug  {
     $self->{'cc'} = \@cc;
   }
 
-  if (Param("useqacontact") && (defined $self->{'qa_contact'}) ) {
-    my $name = $self->{'qa_contact'} > 0 ? &::DBID_to_name($self->{'qa_contact'}) :"";
-    if ($name) {
-      $self->{'qa_contact'} = $name;
-    }
-  }
-
   if (@::legal_keywords) {
     &::SendSQL("SELECT keyworddefs.name 
               FROM keyworddefs, keywords
@@ -172,39 +191,43 @@ sub initBug  {
     }
   }
 
-    &::SendSQL("select attach_id, creation_ts, isprivate, description 
-             from attachments 
-             where bug_id = $bug_id");
-    my @attachments;
-    while (&::MoreSQLData()) {
-        my ($attachid, $date, $isprivate, $desc) = (&::FetchSQLData());
-        my %attach;
-        $attach{'attachid'} = $attachid;
-        $attach{'isprivate'} = $isprivate;
-        $attach{'date'} = $date;
-        $attach{'desc'} = $desc;
-        push @attachments, \%attach;
-    }
-    if (@attachments) {
-        $self->{'attachments'} = \@attachments;
-    }
-
-    &::SendSQL("select bug_id, who, bug_when, isprivate, thetext 
-           from longdescs 
-           where bug_id = $bug_id");
-    my @longdescs;
-    while (&::MoreSQLData()) {
-        my ($bug_id, $who, $bug_when, $isprivate, $thetext) = (&::FetchSQLData());
-        my %longdesc;
-        $longdesc{'who'} = $who;
-        $longdesc{'bug_when'} = $bug_when;
-        $longdesc{'isprivate'} = $isprivate;
-        $longdesc{'thetext'} = $thetext;
-        push @longdescs, \%longdesc;
-    }
-    if (@longdescs) {
-        $self->{'longdescs'} = \@longdescs;
-    }
+  $self->{'attachments'} = Attachment::query($self->{bug_id});
+
+  # The types of flags that can be set on this bug.
+  # If none, no UI for setting flags will be displayed.
+  my $flag_types = 
+    Bugzilla::FlagType::match({ 'target_type'  => 'bug', 
+                                'product_id'   => $self->{'product_id'}, 
+                                'component_id' => $self->{'component_id'} });
+  foreach my $flag_type (@$flag_types) {
+      $flag_type->{'flags'} = 
+        Bugzilla::Flag::match({ 'bug_id'      => $self->{bug_id},
+                                'type_id'     => $flag_type->{'id'},
+                                'target_type' => 'bug' });
+  }
+  $self->{'flag_types'} = $flag_types;
+  $self->{'any_flags_requesteeable'} = grep($_->{'is_requesteeble'}, @$flag_types);
+
+  # The number of types of flags that can be set on attachments to this bug
+  # and the number of flags on those attachments.  One of these counts must be
+  # greater than zero in order for the "flags" column to appear in the table
+  # of attachments.
+  my $num_attachment_flag_types =
+    Bugzilla::FlagType::count({ 'target_type'  => 'attachment',
+                                'product_id'   => $self->{'product_id'},
+                                'component_id' => $self->{'component_id'},
+                                'is_active'    => 1 });
+  my $num_attachment_flags =
+    Bugzilla::Flag::count({ 'target_type'  => 'attachment',
+                            'bug_id'       => $self->{bug_id} });
+
+  $self->{'show_attachment_flags'}
+    = $num_attachment_flag_types || $num_attachment_flags;
+
+  $self->{'milestoneurl'} = $::milestoneurl{$self->{product}};
+
+  $self->{'isunconfirmed'} = ($self->{bug_status} eq $::unconfirmedstate);
+  $self->{'isopened'} = &::IsOpenedState($self->{bug_status});
   
   my @depends = EmitDependList("blocked", "dependson", $bug_id);
   if (@depends) {
@@ -218,7 +241,191 @@ sub initBug  {
   return $self;
 }
 
+sub actual_time {
+    my ($self) = @_;
+
+    return $self->{'actual_time'} if exists $self->{'actual_time'};
+
+    if (&::UserInGroup(Param("timetrackinggroup"))) {
+        &::SendSQL("SELECT SUM(work_time)
+               FROM longdescs WHERE longdescs.bug_id=$self->{bug_id}");
+        $self->{'actual_time'} = &::FetchSQLData();
+    }
+
+    return $self->{'actual_time'};
+}
+
+sub longdescs {
+    my ($self) = @_;
+
+    return $self->{'longdescs'} if exists $self->{'longdescs'};
+
+    $self->{'longdescs'} = &::GetComments($self->{bug_id});
+
+    return $self->{'longdescs'};
+}
+
+sub use_keywords {
+    return @::legal_keywords;
+}
+
+sub use_votes {
+    my ($self) = @_;
+
+    return Param('usevotes')
+      && $::prodmaxvotes{$self->{product}} > 0;
+}
+
+sub groups {
+    my $self = shift;
+
+    return $self->{'groups'} if exists $self->{'groups'};
+
+    my @groups;
+
+    # Some of this stuff needs to go into Bugzilla::User
+
+    # For every group, we need to know if there is ANY bug_group_map
+    # record putting the current bug in that group and if there is ANY
+    # user_group_map record putting the user in that group.
+    # The LEFT JOINs are checking for record existence.
+    #
+    &::SendSQL("SELECT DISTINCT groups.id, name, description," .
+             " bug_group_map.group_id IS NOT NULL," .
+             " user_group_map.group_id IS NOT NULL," .
+             " isactive, membercontrol, othercontrol" .
+             " FROM groups" . 
+             " LEFT JOIN bug_group_map" .
+             " ON bug_group_map.group_id = groups.id" .
+             " AND bug_id = $self->{'bug_id'}" .
+             " LEFT JOIN user_group_map" .
+             " ON user_group_map.group_id = groups.id" .
+             " AND user_id = $::userid" .
+             " AND NOT isbless" .
+             " LEFT JOIN group_control_map" .
+             " ON group_control_map.group_id = groups.id" .
+             " AND group_control_map.product_id = " . $self->{'product_id'} .
+             " WHERE isbuggroup");
+
+    while (&::MoreSQLData()) {
+        my ($groupid, $name, $description, $ison, $ingroup, $isactive,
+            $membercontrol, $othercontrol) = &::FetchSQLData();
+
+        $membercontrol ||= 0;
+
+        # For product groups, we only want to use the group if either
+        # (1) The bit is set and not required, or
+        # (2) The group is Shown or Default for members and
+        #     the user is a member of the group.
+        if ($ison ||
+            ($isactive && $ingroup
+                       && (($membercontrol == CONTROLMAPDEFAULT)
+                           || ($membercontrol == CONTROLMAPSHOWN))
+            ))
+        {
+            my $ismandatory = $isactive
+              && ($membercontrol == CONTROLMAPMANDATORY);
+
+            push (@groups, { "bit" => $groupid,
+                             "ison" => $ison,
+                             "ingroup" => $ingroup,
+                             "mandatory" => $ismandatory,
+                             "description" => $description });
+        }
+    }
+
+    $self->{'groups'} = \@groups;
+
+    return $self->{'groups'};
+}
+
+sub user {
+    my $self = shift;
+    return $self->{'user'} if exists $self->{'user'};
+
+    $self->{'user'} = {};
+
+    my $movers = Param("movers");
+    $self->{'user'}->{'canmove'} = Param("move-enabled") 
+      && (defined $::COOKIE{"Bugzilla_login"}) 
+        && ($::COOKIE{"Bugzilla_login"} =~ /$movers/);
+
+    # In the below, if the person hasn't logged in ($::userid == 0), then
+    # we treat them as if they can do anything.  That's because we don't
+    # know why they haven't logged in; it may just be because they don't
+    # use cookies.  Display everything as if they have all the permissions
+    # in the world; their permissions will get checked when they log in
+    # and actually try to make the change.
+    $self->{'user'}->{'canedit'} = $::userid == 0
+                                   || $::userid == $self->{'reporter'}
+                                   || $::userid == $self->{'qa_contact'}
+                                   || $::userid == $self->{'assigned_to'}
+                                   || &::UserInGroup("editbugs");
+    $self->{'user'}->{'canconfirm'} = ($::userid == 0)
+                                   || &::UserInGroup("canconfirm")
+                                   || &::UserInGroup("editbugs");
+
+    return $self->{'user'};
+}
+
+sub choices {
+    my $self = shift;
+    return $self->{'choices'} if exists $self->{'choices'};
+
+    &::GetVersionTable();
+
+    $self->{'choices'} = {};
 
+    # Fiddle the product list.
+    my $seen_curr_prod;
+    my @prodlist;
+
+    foreach my $product (@::enterable_products) {
+        if ($product eq $self->{'product'}) {
+            # if it's the product the bug is already in, it's ALWAYS in
+            # the popup, period, whether the user can see it or not, and
+            # regardless of the disallownew setting.
+            $seen_curr_prod = 1;
+            push(@prodlist, $product);
+            next;
+        }
+
+        if (!&::CanEnterProduct($product)) {
+            # If we're using bug groups to restrict entry on products, and
+            # this product has an entry group, and the user is not in that
+            # group, we don't want to include that product in this list.
+            next;
+        }
+
+        push(@prodlist, $product);
+    }
+
+    # The current product is part of the popup, even if new bugs are no longer
+    # allowed for that product
+    if (!$seen_curr_prod) {
+        push (@prodlist, $self->{'product'});
+        @prodlist = sort @prodlist;
+    }
+
+    # Hack - this array contains "". See bug 106589.
+    my @res = grep ($_, @::settable_resolution);
+
+    $self->{'choices'} =
+      {
+       'product' => \@prodlist,
+       'rep_platform' => \@::legal_platform,
+       'priority' => \@::legal_priority,
+       'bug_severity' => \@::legal_severity,
+       'op_sys' => \@::legal_opsys,
+       'bug_status' => \@::legal_bugs_status,
+       'resolution' => \@res,
+       'component' => $::components{$self->{product}},
+       'version' => $::versions{$self->{product}},
+       'target_milestone' => $::target_milestone{$self->{product}},
+      };
+
+    return $self->{'choices'};
+}
 
 # given a bug hash, emit xml for it. with file header provided by caller
 #
@@ -261,11 +468,11 @@ sub emitXML {
                      && Param("insidergroup")
                      && !&::UserInGroup(Param("insidergroup")));
             $xml .= "  <long_desc>\n"; 
-            $xml .= "   <who>" . &::DBID_to_name($self->{'longdescs'}[$i]->{'who'}) 
+            $xml .= "   <who>" . $self->{'longdescs'}[$i]->{'email'} 
                                . "</who>\n"; 
-            $xml .= "   <bug_when>" . $self->{'longdescs'}[$i]->{'bug_when'} 
+            $xml .= "   <bug_when>" . $self->{'longdescs'}[$i]->{'time'} 
                                     . "</bug_when>\n"; 
-            $xml .= "   <thetext>" . QuoteXMLChars($self->{'longdescs'}[$i]->{'thetext'})
+            $xml .= "   <thetext>" . QuoteXMLChars($self->{'longdescs'}[$i]->{'body'})
                                    . "</thetext>\n"; 
             $xml .= "  </long_desc>\n"; 
         }
@@ -280,7 +487,7 @@ sub emitXML {
             $xml .= "    <attachid>" . $self->{'attachments'}[$i]->{'attachid'}
                                     . "</attachid>\n"; 
             $xml .= "    <date>" . $self->{'attachments'}[$i]->{'date'} . "</date>\n"; 
-            $xml .= "    <desc>" . QuoteXMLChars($self->{'attachments'}[$i]->{'desc'}) . "</desc>\n"; 
+            $xml .= "    <desc>" . QuoteXMLChars($self->{'attachments'}[$i]->{'description'}) . "</desc>\n"; 
           # $xml .= "    <type>" . $self->{'attachments'}[$i]->{'type'} . "</type>\n"; 
           # $xml .= "    <data>" . $self->{'attachments'}[$i]->{'data'} . "</data>\n"; 
             $xml .= "  </attachment>\n"; 
@@ -310,8 +517,8 @@ sub QuoteXMLChars {
   $_[0] =~ s/&/&amp;/g;
   $_[0] =~ s/</&lt;/g;
   $_[0] =~ s/>/&gt;/g;
-  $_[0] =~ s/'/&apos;/g;
-  $_[0] =~ s/"/&quot;/g;
+  $_[0] =~ s/\'/&apos;/g;
+  $_[0] =~ s/\"/&quot;/g;
 # $_[0] =~ s/([\x80-\xFF])/&XmlUtf8Encode(ord($1))/ge;
   return($_[0]);
 }
@@ -341,156 +548,25 @@ sub XML_Footer {
   return ("</bugzilla>\n");
 }
 
-sub CanChangeField {
-   my $self = shift();
-   my ($f, $oldvalue, $newvalue) = (@_);
-   my $UserInEditGroupSet = -1;
-   my $UserInCanConfirmGroupSet = -1;
-   my $ownerid;
-   my $reporterid;
-   my $qacontactid;
-
-    if ($f eq "assigned_to" || $f eq "reporter" || $f eq "qa_contact") {
-        if ($oldvalue =~ /^\d+$/) {
-            if ($oldvalue == 0) {
-                $oldvalue = "";
-            } else {
-                $oldvalue = &::DBID_to_name($oldvalue);
-            }
-        }
-    }
-    if ($oldvalue eq $newvalue) {
-        return 1;
-    }
-    if (trim($oldvalue) eq trim($newvalue)) {
-        return 1;
-    }
-    if ($f =~ /^longdesc/) {
-        return 1;
-    }
-    if ($UserInEditGroupSet < 0) {
-        $UserInEditGroupSet = UserInGroup($self, "editbugs");
-    }
-    if ($UserInEditGroupSet) {
-        return 1;
-    }
-    &::SendSQL("SELECT reporter, assigned_to, qa_contact FROM bugs " .
-                "WHERE bug_id = $self->{'bug_id'}");
-    ($reporterid, $ownerid, $qacontactid) = (&::FetchSQLData());
-
-    # Let reporter change bug status, even if they can't edit bugs.
-    # If reporter can't re-open their bug they will just file a duplicate.
-    # While we're at it, let them close their own bugs as well.
-    if ( ($f eq "bug_status") && ($self->{'whoid'} eq $reporterid) ) {
-        return 1;
-    }
-    if ($f eq "bug_status" && $newvalue ne $::unconfirmedstate &&
-        &::IsOpenedState($newvalue)) {
-
-        # Hmm.  They are trying to set this bug to some opened state
-        # that isn't the UNCONFIRMED state.  Are they in the right
-        # group?  Or, has it ever been confirmed?  If not, then this
-        # isn't legal.
-
-        if ($UserInCanConfirmGroupSet < 0) {
-            $UserInCanConfirmGroupSet = &::UserInGroup("canconfirm");
-        }
-        if ($UserInCanConfirmGroupSet) {
-            return 1;
-        }
-        &::SendSQL("SELECT everconfirmed FROM bugs WHERE bug_id = $self->{'bug_id'}");
-        my $everconfirmed = FetchOneColumn();
-        if ($everconfirmed) {
-            return 1;
-        }
-    } elsif ($reporterid eq $self->{'whoid'} || $ownerid eq $self->{'whoid'} ||
-             $qacontactid eq $self->{'whoid'}) {
-        return 1;
-    }
-    $self->{'error'} = "
-Only the owner or submitter of the bug, or a sufficiently
-empowered user, may make that change to the $f field."
-}
-
-sub Collision {
-    my $self = shift();
-    my $write = "WRITE";        # Might want to make a param to control
-                                # whether we do LOW_PRIORITY ...
-    &::SendSQL("LOCK TABLES bugs $write, bugs_activity $write, cc $write, " .
-               "cc AS selectVisible_cc $write, " .
-            "profiles $write, dependencies $write, votes $write, " .
-            "keywords $write, longdescs $write, fielddefs $write, " .
-            "keyworddefs READ, groups READ, attachments READ, products READ");
-    &::SendSQL("SELECT delta_ts FROM bugs where bug_id=$self->{'bug_id'}");
-    my $delta_ts = &::FetchOneColumn();
-    &::SendSQL("unlock tables");
-    if ($self->{'delta_ts'} ne $delta_ts) {
-       return 1;
-    }
-    else {
-       return 0;
-    }
-}
-
-sub AppendComment  {
-    my $self = shift();
-    my ($comment) = (@_);
-    $comment =~ s/\r\n/\n/g;     # Get rid of windows-style line endings.
-    $comment =~ s/\r/\n/g;       # Get rid of mac-style line endings.
-    if ($comment =~ /^\s*$/) {  # Nothin' but whitespace.
-        return;
-    }
-
-    &::SendSQL("INSERT INTO longdescs (bug_id, who, bug_when, thetext) " .
-            "VALUES($self->{'bug_id'}, $self->{'whoid'}, now(), " . &::SqlQuote($comment) . ")");
-
-    &::SendSQL("UPDATE bugs SET delta_ts = now() WHERE bug_id = $self->{'bug_id'}");
-}
-
-
-#from o'reilley's Programming Perl
-sub display {
-    my $self = shift;
-    my @keys;
-    if (@_ == 0) {                  # no further arguments
-        @keys = sort keys(%$self);
-    }  else {
-        @keys = @_;                 # use the ones given
-    }
-    foreach my $key (@keys) {
-        print "\t$key => $self->{$key}\n";
-    }
-}
-
-sub CommitChanges {
-
-#snapshot bug
-#snapshot dependencies
-#check can change fields
-#check collision
-#lock and change fields
-#notify through mail
-
-}
-
 sub AUTOLOAD {
   use vars qw($AUTOLOAD);
-  my $self = shift;
-  my $type = ref($self) || $self;
   my $attr = $AUTOLOAD;
 
   $attr =~ s/.*:://;
   return unless $attr=~ /[^A-Z]/;
-  if (@_) {
-    $self->{$attr} = shift;
-    return;
-  }
   confess ("invalid bug attribute $attr") unless $ok_field{$attr};
-  if (defined $self->{$attr}) {
-    return $self->{$attr};
-  } else {
-    return '';
-  }
+
+  no strict 'refs';
+  *$AUTOLOAD = sub {
+      my $self = shift;
+      if (defined $self->{$attr}) {
+          return $self->{$attr};
+      } else {
+          return '';
+      }
+  };
+
+  goto &$AUTOLOAD;
 }
 
 1;
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index a45cf0976..46f520b77 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -59,7 +59,7 @@ sub new {
     }
     
     $self->{'name'} = $name;
-    $self->{'email'} = $email;
+    $self->{'email'} = $email || "__UNKNOWN__";
     $self->{'exists'} = $exists;
         
     # Generate a string to identify the user by name + email if the user
diff --git a/bug_form.pl b/bug_form.pl
deleted file mode 100644
index b4a2ef678..000000000
--- a/bug_form.pl
+++ /dev/null
@@ -1,398 +0,0 @@
-# -*- 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>
-#                 Dave Miller <justdave@syndicomm.com>
-#                 Vaskin Kissoyan <vkissoyan@yahoo.com>
-
-use strict;
-
-use RelationSet;
-use Bugzilla::Constants;
-# Use the Attachment module to display attachments for the bug.
-use Attachment;
-
-# Use the Flag modules to display flags on the bug.
-use Bugzilla::Flag;
-use Bugzilla::FlagType;
-
-sub show_bug {    
-    # Shut up misguided -w warnings about "used only once".  For some reason,
-    # "use vars" chokes on me when I try it here.
-    sub bug_form_pl_sillyness {
-        my $zz;
-        $zz = %::FORM;
-        $zz = %::proddesc;
-        $zz = %::prodmaxvotes;
-        $zz = @::enterable_products;                                            
-        $zz = @::settable_resolution;
-        $zz = $::unconfirmedstate;
-        $zz = $::milestoneurl;
-        $zz = $::template;
-        $zz = $::vars;
-        $zz = @::legal_priority;
-        $zz = @::legal_platform;
-        $zz = @::legal_severity;
-        $zz = @::legal_bug_status;
-        $zz = @::target_milestone;
-        $zz = @::components;
-        $zz = @::legal_keywords;
-        $zz = @::versions;
-        $zz = @::legal_opsys;
-    }
-
-    # Use templates
-    my $template = $::template;
-    my $vars = $::vars;
-    
-    $vars->{'GetBugLink'} = \&GetBugLink;
-    $vars->{'lsearch'} = \&lsearch,
-    $vars->{'header_done'} = (@_),
-
-    quietly_check_login();
-
-    my $id = $::FORM{'id'};
-    
-    if (!defined($id)) {
-      $template->process("bug/choose.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
-      exit;
-    }
-    
-    my %user = %{$vars->{'user'}};
-    my %bug;
-
-    # Populate the bug hash with the info we get directly from the DB.
-    my $query = "
-    SELECT bugs.bug_id, alias, bugs.product_id, products.name, version, 
-        rep_platform, op_sys, bug_status, resolution, priority, 
-        bug_severity, bugs.component_id, components.name, assigned_to, 
-        reporter, bug_file_loc, short_desc, target_milestone, 
-        qa_contact, status_whiteboard, 
-        DATE_FORMAT(creation_ts,'%Y.%m.%d %H:%i'), delta_ts, sum(votes.count),
-        estimated_time, remaining_time
-    FROM bugs LEFT JOIN votes USING(bug_id), products, components
-    WHERE bugs.bug_id = $id
-        AND bugs.product_id = products.id
-        AND bugs.component_id = components.id
-    GROUP BY bugs.bug_id";
-
-    SendSQL($query);
-
-    # The caller is meant to have checked this. Abort here so that
-    # we don't get obscure SQL errors, below
-    if (!MoreSQLData()) {
-        $vars->{'bug_id'} = $id;
-        ThrowCodeError("no_bug_data");
-    }
-
-    my $value;
-    my $disp_date;
-    my @row = FetchSQLData();
-    foreach my $field ("bug_id", "alias", "product_id", "product", "version", 
-                       "rep_platform", "op_sys", "bug_status", "resolution", 
-                       "priority", "bug_severity", "component_id", "component", 
-                       "assigned_to", "reporter", "bug_file_loc", "short_desc", 
-                       "target_milestone", "qa_contact", "status_whiteboard", 
-                       "creation_ts", "delta_ts", "votes", 
-                       "estimated_time", "remaining_time")
-    {
-        $value = shift(@row);
-        $bug{$field} = defined($value) ? $value : "";
-    }
-
-    # General arrays of info about the database state
-    GetVersionTable();
-
-    # Fiddle the product list.
-    my $seen_curr_prod;
-    my @prodlist;
-    
-    foreach my $product (@::enterable_products) {
-        if ($product eq $bug{'product'}) {
-            # if it's the product the bug is already in, it's ALWAYS in
-            # the popup, period, whether the user can see it or not, and
-            # regardless of the disallownew setting.
-            $seen_curr_prod = 1;
-            push(@prodlist, $product);
-            next;
-        }
-
-        if (!CanEnterProduct($product)) {
-            # If we're using bug groups to restrict entry on products, and
-            # this product has an entry group, and the user is not in that
-            # group, we don't want to include that product in this list.
-            next;
-        }
-
-        push(@prodlist, $product);
-    }
-
-    # The current product is part of the popup, even if new bugs are no longer
-    # allowed for that product
-    if (!$seen_curr_prod) {
-        push (@prodlist, $bug{'product'});
-        @prodlist = sort @prodlist;
-    }
-
-    $vars->{'product'} = \@prodlist;
-    $vars->{'rep_platform'} = \@::legal_platform;
-    $vars->{'priority'} = \@::legal_priority;
-    $vars->{'bug_severity'} = \@::legal_severity;
-    $vars->{'op_sys'} = \@::legal_opsys;
-    $vars->{'bug_status'} = \@::legal_bug_status;
-
-    # Hack - this array contains "" for some reason. See bug 106589.
-    shift @::settable_resolution; 
-    $vars->{'resolution'} = \@::settable_resolution;
-
-    $vars->{'component_'} = $::components{$bug{'product'}};
-    $vars->{'version'} = $::versions{$bug{'product'}};
-    $vars->{'target_milestone'} = $::target_milestone{$bug{'product'}};
-    $bug{'milestoneurl'} = $::milestoneurl{$bug{'product'}} || 
-                           "notargetmilestone.html";
-
-    $vars->{'use_votes'} = Param('usevotes')
-                           && $::prodmaxvotes{$bug{'product'}} > 0;
-
-    # Add additional, calculated fields to the bug hash
-    if (@::legal_keywords) {
-        $vars->{'use_keywords'} = 1;
-
-        SendSQL("SELECT keyworddefs.name 
-                 FROM keyworddefs, keywords
-                 WHERE keywords.bug_id = $id 
-                 AND keyworddefs.id = keywords.keywordid
-                 ORDER BY keyworddefs.name");
-        my @keywords;
-        while (MoreSQLData()) {
-            push(@keywords, FetchOneColumn());
-        }
-
-        $bug{'keywords'} = \@keywords;
-    }    
-
-    # Attachments
-    $bug{'attachments'} = Attachment::query($id);
-   
-    # The types of flags that can be set on this bug.
-    # If none, no UI for setting flags will be displayed.
-    my $flag_types = 
-      Bugzilla::FlagType::match({ 'target_type'  => 'bug', 
-                                  'product_id'   => $bug{'product_id'}, 
-                                  'component_id' => $bug{'component_id'} });
-    foreach my $flag_type (@$flag_types) {
-        $flag_type->{'flags'} = 
-          Bugzilla::Flag::match({ 'bug_id'      => $id , 
-                                  'type_id'     => $flag_type->{'id'} , 
-                                  'target_type' => 'bug' });
-    }
-    $vars->{'flag_types'} = $flag_types;
-    $vars->{'any_flags_requesteeble'} = grep($_->{'is_requesteeble'}, @$flag_types);
-
-    # The number of types of flags that can be set on attachments to this bug
-    # and the number of flags on those attachments.  One of these counts must be
-    # greater than zero in order for the "flags" column to appear in the table 
-    # of attachments.
-    my $num_attachment_flag_types = 
-      Bugzilla::FlagType::count({ 'target_type'  => 'attachment', 
-                                  'product_id'   => $bug{'product_id'}, 
-                                  'component_id' => $bug{'component_id'},
-                                  'is_active'    => 1 });
-    my $num_attachment_flags = 
-      Bugzilla::Flag::count({ 'target_type'  => 'attachment', 
-                              'bug_id'       => $id });
-    
-    $vars->{'show_attachment_flags'} 
-      = $num_attachment_flag_types || $num_attachment_flags;
-
-    # Dependencies
-    my @list;
-    SendSQL("SELECT dependson FROM dependencies WHERE  
-             blocked = $id ORDER BY dependson");
-    while (MoreSQLData()) {
-        my ($i) = FetchSQLData();
-        push(@list, $i);
-    }
-
-    if (UserInGroup(Param("timetrackinggroup"))) {
-
-        SendSQL("SELECT SUM(work_time) 
-                 FROM longdescs WHERE longdescs.bug_id=$id");
-        $bug{'actual_time'} = FetchSQLData();
-
-    }
-
-    $bug{'dependson'} = \@list;
-
-    my @list2;
-    SendSQL("SELECT blocked FROM dependencies WHERE  
-             dependson = $id ORDER BY blocked");
-    while (MoreSQLData()) {
-        my ($i) = FetchSQLData();
-        push(@list2, $i);
-    }
-
-    $bug{'blocked'} = \@list2;
-
-    # Groups
-    my @groups;
-
-    # For every group, we need to know if there is ANY bug_group_map
-    # record putting the current bug in that group and if there is ANY
-    # user_group_map record putting the user in that group.
-    # The LEFT JOINs are checking for record existence.
-    #
-    SendSQL("SELECT DISTINCT groups.id, name, description," .
-             " bug_group_map.group_id IS NOT NULL," .
-             " user_group_map.group_id IS NOT NULL," .
-             " isactive, membercontrol, othercontrol" .
-             " FROM groups" . 
-             " LEFT JOIN bug_group_map" .
-             " ON bug_group_map.group_id = groups.id" .
-             " AND bug_id = $bug{'bug_id'}" .
-             " LEFT JOIN user_group_map" .
-             " ON user_group_map.group_id = groups.id" .
-             " AND user_id = $::userid" .
-             " AND NOT isbless" .
-             " LEFT JOIN group_control_map" .
-             " ON group_control_map.group_id = groups.id" .
-             " AND group_control_map.product_id = " . $bug{'product_id'} .
-             " WHERE isbuggroup");
-
-    $user{'inallgroups'} = 1;
-
-    while (MoreSQLData()) {
-        my ($groupid, $name, $description, $ison, $ingroup, $isactive, 
-            $membercontrol, $othercontrol) = FetchSQLData();
-
-        $bug{'inagroup'} = 1 if ($ison);
-        $membercontrol ||= 0;
-
-        if ($isactive && ($membercontrol == CONTROLMAPMANDATORY)) {
-            $bug{'inagroup'} = 1;
-        }
-        # For product groups, we only want to display the checkbox if either
-        # (1) The bit is set and not required, or
-        # (2) The group is Shown or Default for members and
-        #     the user is a member of the group.
-        if ($ison || 
-           ($isactive && $ingroup 
-                       && (($membercontrol == CONTROLMAPDEFAULT)
-                       || ($membercontrol == CONTROLMAPSHOWN))
-            ))
-        {
-            $user{'inallgroups'} &= $ingroup;
-
-            my $mandatory;
-            if ($isactive && ($membercontrol == CONTROLMAPMANDATORY)) {
-                $mandatory = 1;
-            } else {
-                $mandatory = 0;
-            }
-            if (($ison) || ($ingroup)) {
-                push (@groups, { "bit" => $groupid,
-                                 "ison" => $ison,
-                                 "ingroup" => $ingroup,
-                                 "mandatory" => $mandatory,
-                                 "description" => $description });            
-            }
-        }
-    }
-
-    # If the bug is restricted to a group, get flags that allow
-    # the user to set whether or not the reporter 
-    # and cc list can see the bug even if they are not members of all 
-    # groups to which the bug is restricted.
-    if ($bug{'inagroup'}) {
-
-        # Determine whether or not the bug is always accessible by the
-        # reporter, QA contact, and/or users on the cc: list.
-        SendSQL("SELECT reporter_accessible, cclist_accessible
-                 FROM   bugs
-                 WHERE  bug_id = $id
-                ");
-        ($bug{'reporter_accessible'}, 
-         $bug{'cclist_accessible'}) = FetchSQLData();        
-    }
-    $vars->{'groups'} = \@groups;
-
-    my $movers = Param("movers");
-    $user{'canmove'} = Param("move-enabled") 
-                       && (defined $::COOKIE{"Bugzilla_login"}) 
-                       && ($::COOKIE{"Bugzilla_login"} =~ /$movers/);
-
-    # User permissions
-
-    # In the below, if the person hasn't logged in ($::userid == 0), then
-    # we treat them as if they can do anything.  That's because we don't
-    # know why they haven't logged in; it may just be because they don't
-    # use cookies.  Display everything as if they have all the permissions
-    # in the world; their permissions will get checked when they log in
-    # and actually try to make the change.
-    $user{'canedit'} = $::userid == 0
-                       || $::userid == $bug{'reporter'}
-                       || $::userid == $bug{'qa_contact'}
-                       || $::userid == $bug{'assigned_to'}
-                       || UserInGroup("editbugs");
-    $user{'canconfirm'} = ($::userid == 0)
-                          || UserInGroup("canconfirm")
-                          || UserInGroup("editbugs");
-
-    # Bug states
-    $bug{'isunconfirmed'} = ($bug{'bug_status'} eq $::unconfirmedstate);
-    $bug{'isopened'} = IsOpenedState($bug{'bug_status'});
-
-    # People involved with the bug
-    $bug{'assigned_to_email'} = DBID_to_name($bug{'assigned_to'});
-    $bug{'assigned_to'} = DBID_to_real_or_loginname($bug{'assigned_to'});
-    $bug{'reporter'} = DBID_to_real_or_loginname($bug{'reporter'});
-    $bug{'qa_contact'} = $bug{'qa_contact'} > 0 ? 
-                                          DBID_to_name($bug{'qa_contact'}) : "";
-
-    my $ccset = new RelationSet;
-    $ccset->mergeFromDB("SELECT who FROM cc WHERE bug_id=$id");
-    
-    my @cc = $ccset->toArrayOfStrings();
-    $bug{'cc'} = \@cc if $cc[0];
-
-    # Next bug in list (if there is one)
-    my @bug_list;
-    if ($::COOKIE{"BUGLIST"} && $id) 
-    {
-        @bug_list = split(/:/, $::COOKIE{"BUGLIST"});
-    }
-    $vars->{'bug_list'} = \@bug_list;
-
-    $bug{'comments'} = GetComments($bug{'bug_id'});
-
-    # This is length in number of comments
-    $bug{'longdesclength'} = scalar(@{$bug{'comments'}});
-
-    # Add the bug and user hashes to the variables
-    $vars->{'bug'} = \%bug;
-    $vars->{'user'} = \%user;
-
-    # Generate and return the UI (HTML page) from the appropriate template.
-    $template->process("bug/edit.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-}
- 
-1;
-
diff --git a/checksetup.pl b/checksetup.pl
index 101583096..aceb6a706 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -921,14 +921,6 @@ END
 }
 
 {
-    eval("use Date::Parse");
-    # Templates will be recompiled if the source changes, but not if the
-    # settings in globals.pl change, so we need to be able to force a rebuild
-    # if that happens
-
-    # The last time the global template params were changed. Keep in UTC,
-    # YYYY-MM-DD
-    my $lastTemplateParamChange = str2time("2002-04-27", "UTC");
     if (-e 'data/template') {
         print "Removing existing compiled templates ...\n" unless $silent;
 
@@ -967,6 +959,8 @@ END
          js => sub { return $_; },
          html_linebreak => sub { return $_; },
          url_quote => sub { return $_; },
+         quoteUrls => sub { return $_; },
+         bug_link => [ sub { return sub { return $_; } }, 1],
          csv => sub { return $_; },
          time => sub { return $_; },
         },
diff --git a/globals.pl b/globals.pl
index 547fd1b95..79c2f92b4 100644
--- a/globals.pl
+++ b/globals.pl
@@ -1781,8 +1781,20 @@ $::template ||= Template->new(
         # characters NOT in the regex set: [a-zA-Z0-9_\-.].  The 'uri'
         # filter should be used for a full URL that may have
         # characters that need encoding.
-        url_quote => \&Bugzilla::Util::url_quote,
-        
+        url_quote => \&Bugzilla::Util::url_quote ,
+
+        quoteUrls => \&quoteUrls ,
+
+        bug_link => [ sub {
+                          my ($context, $bug) = @_;
+                          return sub {
+                              my $text = shift;
+                              return GetBugLink($text, $bug);
+                          };
+                      },
+                      1
+                    ],
+
         # In CSV, quotes are doubled, and we enclose the whole value in quotes
         csv => sub
         {
@@ -1892,9 +1904,6 @@ $::vars =
     # Generic linear search function
     'lsearch' => \&Bugzilla::Util::lsearch ,
 
-    # quoteUrls - autolinkifies text
-    'quoteUrls' => \&quoteUrls ,
-    
     # UserInGroup - you probably want to cache this
     'UserInGroup' => \&UserInGroup ,
 
diff --git a/post_bug.cgi b/post_bug.cgi
index 3ddfbe689..2a65c2436 100755
--- a/post_bug.cgi
+++ b/post_bug.cgi
@@ -28,7 +28,8 @@ use lib qw(.);
 
 use Bugzilla::Constants;
 require "CGI.pl";
-require "bug_form.pl";
+
+use Bug;
 
 use Bugzilla::User;
 
@@ -491,28 +492,38 @@ close(PMAIL);
 
 # Tell the user all about it
 $vars->{'id'} = $id;
-$vars->{'mail'} = $mailresults;
-$vars->{'type'} = "created";
+my $bug = new Bug($id, $::userid);
+$vars->{'bug'} = $bug;
 
-print "Content-type: text/html\n\n";
-$template->process("bug/create/created.html.tmpl", $vars)
-  || ThrowTemplateError($template->error());
+ThrowCodeError("bug_error") if $bug->error;
+
+$vars->{'sentmail'} = [];
+
+push (@{$vars->{'sentmail'}}, { type => 'created',
+                                id => $id,
+                                mail => $mailresults
+                              });
 
 foreach my $i (@all_deps) {
-    $vars->{'mail'} = "";
-    open(PMAIL, "-|") or exec('./processmail', $i, $::COOKIE{'Bugzilla_login'});    $vars->{'mail'} .= $_ while <PMAIL>;
+    my $mail = "";
+    open(PMAIL, "-|") or exec('./processmail', $i, $::COOKIE{'Bugzilla_login'});
+    $mail .= $_ while <PMAIL>;
     close(PMAIL);
 
-    $vars->{'id'} = $i;
-    $vars->{'type'} = "dep";
+    push (@{$vars->{'sentmail'}}, { type => 'dep',
+                                    id => $i,
+                                    mail => $mail
+                                  });
+}
 
-    # Let the user know we checked to see if we should email notice
-    # of this new bug to users with a relationship to the depenedant
-    # bug and who did and didn't receive email about it
-    $template->process("bug/process/results.html.tmpl", $vars) 
-      || ThrowTemplateError($template->error());
+my @bug_list;
+if ($::COOKIE{"BUGLIST"}) {
+    @bug_list = split(/:/, $::COOKIE{"BUGLIST"});
 }
+$vars->{'bug_list'} = \@bug_list;
+
+print "Content-type: text/html\n\n";
+$template->process("bug/create/created.html.tmpl", $vars)
+  || ThrowTemplateError($template->error());
 
-$::FORM{'id'} = $id;
 
-show_bug("header is already done");
diff --git a/process_bug.cgi b/process_bug.cgi
index a59e439dc..80b318a74 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -33,8 +33,8 @@ use lib qw(.);
 
 use Bugzilla::Constants;
 require "CGI.pl";
-require "bug_form.pl";
 
+use Bug;
 use Bugzilla::User;
 
 use RelationSet;
@@ -53,7 +53,7 @@ use vars qw(%versions
           %settable_resolution
           %target_milestone
           %legal_severity
-          $next_bug);
+           );
 
 ConnectToDatabase();
 my $whoid = confirm_login();
@@ -151,6 +151,23 @@ if (defined($::FORM{'id'})) {
     }
 }
 
+# Set up the vars for nagiavtional <link> elements
+my $next_bug;
+if ($::COOKIE{"BUGLIST"} && $::FORM{'id'}) {
+    my @bug_list = split(/:/, $::COOKIE{"BUGLIST"});
+    $vars->{'bug_list'} = \@bug_list;
+    my $cur = lsearch(\@bug_list, $::FORM{"id"});
+    if ($cur >= 0 && $cur < $#bug_list) {
+        $next_bug = $bug_list[$cur + 1];
+
+        # Note that we only bother with the bug_id here, and get
+        # the full bug object at the end, before showing the edit
+        # page. If you change this, remember that we have not
+        # done the security checks on the next bug yet
+        $vars->{'bug'} = { bug_id => $next_bug };
+    }
+}
+
 # Start displaying the response page.
 $template->process("bug/process/header.html.tmpl", $vars)
   || ThrowTemplateError($template->error());
@@ -1725,26 +1742,17 @@ foreach my $id (@idlist) {
     }
 }
 
-# Show next bug, if it exists.
-if ($::COOKIE{"BUGLIST"} && $::FORM{'id'}) {
-    my @bugs = split(/:/, $::COOKIE{"BUGLIST"});
-    $vars->{'bug_list'} = \@bugs;
-    my $cur = lsearch(\@bugs, $::FORM{"id"});
-    if ($cur >= 0 && $cur < $#bugs) {
-        my $next_bug = $bugs[$cur + 1];
-        if (detaint_natural($next_bug) && CanSeeBug($next_bug, $::userid)) {
-            $::FORM{'id'} = $next_bug;
-            
-            $vars->{'next_id'} = $next_bug;
-            
-            # Let the user know we are about to display the next bug in their list.
-            $template->process("bug/process/next.html.tmpl", $vars)
-              || ThrowTemplateError($template->error());
+# now show the next bug
+if ($next_bug) {
+    if (detaint_natural($next_bug) && CanSeeBug($next_bug, $::userid)) {
+        my $bug = new Bug($next_bug, $::userid);
+        $vars->{'bug'} = $bug;
+        ThrowCodeError("bug_error") if $bug->error;
 
-            show_bug("header is already done");
+        $template->process("bug/process/next.html.tmpl", $vars)
+          || ThrowTemplateError($template->error());
 
-            exit;
-        }
+        exit;
     }
 }
 
diff --git a/show_bug.cgi b/show_bug.cgi
index 6711ff7e6..377c7905d 100755
--- a/show_bug.cgi
+++ b/show_bug.cgi
@@ -25,10 +25,13 @@ use strict;
 use lib qw(.);
 
 require "CGI.pl";
-require "bug_form.pl";
 
 ConnectToDatabase();
 
+use vars qw($template $vars $userid);
+
+use Bug;
+
 if ($::FORM{'GoAheadAndLogIn'}) {
     confirm_login();
 } else {
@@ -39,11 +42,20 @@ if ($::FORM{'GoAheadAndLogIn'}) {
 # Begin Data/Security Validation
 ######################################################################
 
+unless (defined ($::FORM{'id'})) {
+    my $format = GetFormat("bug/choose", $::FORM{'format'}, $::FORM{'ctype'});
+
+    print "Content-type: $format->{'contenttype'}\n\n";
+    $template->process("$format->{'template'}", $vars) ||
+      ThrowTemplateError($template->error());
+    exit;
+}
+
+my $format = GetFormat("bug/show", $::FORM{'format'}, $::FORM{'ctype'});
+
 # Make sure the bug ID is a positive integer representing an existing
 # bug that the user is authorized to access.
-if (defined ($::FORM{'id'})) {
-    ValidateBugID($::FORM{'id'});
-}
+ValidateBugID($::FORM{'id'});
 
 ######################################################################
 # End Data/Security Validation
@@ -51,6 +63,20 @@ if (defined ($::FORM{'id'})) {
 
 GetVersionTable();
 
-print "Content-type: text/html\n\n";
+my $bug = new Bug($::FORM{'id'}, $userid);
+
+$vars->{'bug'} = $bug;
+
+ThrowCodeError("bug_error") if $bug->error;
+
+# Next bug in list (if there is one)
+my @bug_list;
+if ($::COOKIE{"BUGLIST"}) {
+    @bug_list = split(/:/, $::COOKIE{"BUGLIST"});
+}
+$vars->{'bug_list'} = \@bug_list;
+
+print "Content-type: $format->{'ctype'}\n\n";
+$template->process("$format->{'template'}", $vars)
+  || ThrowTemplateError($template->error());
 
-show_bug();
diff --git a/t/004template.t b/t/004template.t
index 6c44fca48..b3fdcc8b7 100644
--- a/t/004template.t
+++ b/t/004template.t
@@ -81,6 +81,8 @@ my $provider = Template::Provider->new(
         js        => sub { return $_ } ,
         strike    => sub { return $_ } ,
         url_quote => sub { return $_ } ,
+        quoteUrls => sub { return $_ } ,
+        bug_link => [ sub { return sub { return $_; } }, 1] ,
         csv       => sub { return $_ } ,
         time      => sub { return $_ } ,
     },
diff --git a/template/en/default/bug/comments.html.tmpl b/template/en/default/bug/comments.html.tmpl
index 42971b327..98d7ae386 100644
--- a/template/en/default/bug/comments.html.tmpl
+++ b/template/en/default/bug/comments.html.tmpl
@@ -68,7 +68,7 @@
   # generated HTML
   #%]
 <pre>
-  [%- quoteUrls(comment.body) -%]
+  [%- comment.body FILTER quoteUrls -%]
 </pre>
     </div>
   [% END %]
diff --git a/template/en/default/bug/create/created.html.tmpl b/template/en/default/bug/create/created.html.tmpl
index 0264413a7..5966e4e0e 100644
--- a/template/en/default/bug/create/created.html.tmpl
+++ b/template/en/default/bug/create/created.html.tmpl
@@ -23,8 +23,24 @@
   title = "Bug $id Submitted"
 %]
 
-[% PROCESS bug/process/results.html.tmpl %]
+[% FOREACH item = sentmail %]
+  [% PROCESS bug/process/results.html.tmpl
+     type = item.type
+     id = item.id
+     mail = item.mail
+   %]
+[% END %]
 
 <br>
 
-[%# post_bug.cgi will add a copy of the filed bug below here %]
+<hr>
+
+[% PROCESS bug/edit.html.tmpl %]
+
+<hr>
+
+[% PROCESS bug/navigate.html.tmpl %]
+
+<br>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/bug/edit.html.tmpl b/template/en/default/bug/edit.html.tmpl
index aa56678dd..56483a779 100644
--- a/template/en/default/bug/edit.html.tmpl
+++ b/template/en/default/bug/edit.html.tmpl
@@ -20,19 +20,6 @@
   #                 Vaskin Kissoyan <vkissoyan@yahoo.com> 
   #%]
 
-[% filtered_desc = bug.short_desc FILTER html %]
-[% filtered_timestamp = bug.delta_ts FILTER time %]
-[% UNLESS header_done %]
-  [% PROCESS global/header.html.tmpl 
-    title = "Bug $bug.bug_id - $bug.short_desc"
-    h1 = "Bugzilla Bug $bug.bug_id"
-    h2 = filtered_desc
-    h3 = "Last modified: $filtered_timestamp"
-    style_urls = [ "css/edit_bug.css" ]
-   %]
-[% END %]
-
-[% PROCESS bug/navigate.html.tmpl %]
 [% PROCESS bug/time.html.tmpl %]
 
 [% IF UserInGroup(Param('timetrackinggroup')) %]
@@ -59,12 +46,10 @@
   </script>
 [% END %]
 
-<hr>
-
 <form name="changeform" method="post" action="process_bug.cgi">
 
   <input type="hidden" name="delta_ts" value="[% bug.delta_ts %]">
-  <input type="hidden" name="longdesclength" value="[% bug.longdesclength %]">
+  <input type="hidden" name="longdesclength" value="[% bug.longdescs.size %]">
   <input type="hidden" name="id" value="[% bug.bug_id %]">
 
 [%# *** Hardware Reporter Product OS AddCC *** %]
@@ -96,7 +81,7 @@
         <b>Reporter:</b>
       </td>
       <td>
-        [% bug.reporter FILTER html %]
+        [% bug.reporter.identity FILTER html %]
       </td>    
     </tr>
     
@@ -128,19 +113,7 @@
             Co<u>m</u>ponent</a>:
         </b>
       </td>
-      <td>
-        <label for="component" accesskey="m">
-          <select name="component" id="component">
-            [% FOREACH x = component_ %]
-              <option value="[% x FILTER html %]"
-                [% " selected" IF x == bug.component %]>[% x FILTER html %]
-              </option>
-            [% END %]
-          </select>
-        </label>
-      </td>
-
-      <td>&nbsp;</td>
+      [% PROCESS select selname => "component" accesskey => "m" %]
  
       <td align="right">
         <b><u>V</u>ersion:</b>
@@ -203,14 +176,17 @@
           <a href="bug_status.html#assigned_to">Assigned&nbsp;To</a>:
         </b>
       </td>
-      <td>[% bug.assigned_to FILTER html %]</td>
+      <td>[% bug.assigned_to.identity FILTER html %]</td>
       <td>&nbsp;</td>
  
       [% IF Param("usetargetmilestone") && bug.target_milestone %]
         <td align="right">
           <b>
-            <a href="[% bug.milestoneurl FILTER html %]"><u>T</u>arget
-            Milestone</a>:
+            [% IF bug.milestoneurl %]
+              <a href="[% bug.milestoneurl FILTER html %]">
+            [% END %]
+            <u>T</u>arget Milestone</a>:
+            [% "</a>" IF bug.milestoneurl %]
           </b>
         </td>
         [% PROCESS select selname = "target_milestone" accesskey => "t" %]
@@ -228,7 +204,7 @@
        </td>
        <td colspan="7">
          <input name="qa_contact" accesskey="q"
-                value="[% bug.qa_contact FILTER html %]" size="60">
+                value="[% bug.qa_contact.email FILTER html %]" size="60">
        </td>
      </tr>
    [% END %]
@@ -248,8 +224,10 @@
              value="[% bug.bug_file_loc FILTER html %]" size="60">
     </td>
     <td rowspan="4" colspan="2" valign="top"> 
-        [% IF flag_types.size > 0 %]
-          [% PROCESS "flag/list.html.tmpl" %]
+        [% IF bug.flag_types.size > 0 %]
+          [% PROCESS "flag/list.html.tmpl"
+             flag_types = bug.flag_types
+             any_flags_requesteeble = bug.any_flags_requesteeble %]
         [% END %]
     </td>
   </tr>
@@ -276,7 +254,7 @@
     </tr>
   [% END %]
 
-  [% IF use_keywords %]
+  [% IF bug.use_keywords %]
     <tr>
       <td align="right">
         <b>
@@ -350,7 +328,10 @@
 
   [% PROCESS attachment/list.html.tmpl 
              attachments = bug.attachments 
-             bugid       = bug.bug_id  %]
+             bugid       = bug.bug_id
+             num_attachment_flag_types = bug.num_attachment_flag_types
+             show_attachment_flags = bug.show_attachment_flags
+   %]
 
 [%# *** Dependencies Votes *** %]
 
@@ -405,9 +386,13 @@
             accesskey="c"></textarea>
   <br>
 
-  [% IF groups.size > 0 %]
+  [% IF bug.groups.size > 0 %]
+    [% inallgroups = 1 %]
+    [% inagroup = 0 %]
+    [% FOREACH group = bug.groups %]
+      [% SET inallgroups = 0 IF NOT group.ingroup %]
+      [% SET inagroup = 1 IF group.ison %]
 
-    [% FOREACH group = groups %]
       [% IF NOT group.mandatory %]
         [% IF NOT emitted_description %]
           [% emitted_description = 1 %]
@@ -430,7 +415,7 @@
       [% END %]
     [% END %]
 
-    [% IF NOT user.inallgroups %]
+    [% IF NOT inallgroups %]
       <b>
         Only members of a group can change the visibility of a bug for 
         that group
@@ -438,7 +423,7 @@
     <br>
     [% END %]
 
-    [% IF bug.inagroup %]
+    [% IF inagroup %]
       <p>
         <b>Users in the roles selected below can always view this bug:</b>
         <br>
@@ -472,16 +457,16 @@
   [% knum = 1 %]
 
   [% IF bug.bug_status == "UNCONFIRMED" && 
-        user.canconfirm %]
+        bug.user.canconfirm %]
     <input type="radio" name="knob" value="confirm">
     Confirm bug (change status to <b>NEW</b>)
     <br>
     [% knum = knum + 1 %]
   [% END %]
 
-  [% IF user.canedit %]
+  [% IF bug.user.canedit %]
     [% IF bug.isopened %]
-      [% IF bug.bug_status != "ASSIGNED" && user.canconfirm %]
+      [% IF bug.bug_status != "ASSIGNED" && bug.user.canconfirm %]
         <input type="radio" name="knob" value="accept">
         Accept bug (
         [% "confirm bug, " IF bug.isunconfirmed %]change
@@ -501,7 +486,7 @@
       Resolve bug, changing <a href="bug_status.html">resolution</a> to
       <select name="resolution" 
               onchange="document.changeform.knob[[% knum %]].checked=true">
-      [% FOREACH r = resolution %]
+      [% FOREACH r = bug.choices.resolution %]
         <option value="[% r FILTER html %]">[% r FILTER html %]</option>
       [% END %]
       </select>
@@ -523,9 +508,9 @@
                             (this.value != '')) {
                          document.changeform.knob[[% knum %]].checked=true; 
                        }"  
-             value="[% bug.assigned_to_email FILTER html %]">
+             value="[% bug.assigned_to.email FILTER html %]">
       <br>
-      [% IF bug.isunconfirmed && user.canconfirm %]
+      [% IF bug.isunconfirmed && bug.user.canconfirm %]
         &nbsp;&nbsp;&nbsp;&nbsp;<input type="checkbox" name="andconfirm">
         and confirm bug (change status to <b>NEW</b>)
         <br>
@@ -537,7 +522,7 @@
       [% " and QA contact" IF Param('useqacontact') %]
       of selected component
       <br>
-      [% IF bug.isunconfirmed && user.canconfirm %]
+      [% IF bug.isunconfirmed && bug.user.canconfirm %]
         &nbsp;&nbsp;&nbsp;&nbsp;<input type="checkbox" name="compconfirm">
         and confirm bug (change status to <b>NEW</b>)
         <br>
@@ -545,7 +530,7 @@
       [% knum = knum + 1 %]
     [% ELSE %]
       [% IF bug.resolution != "MOVED" ||
-           (bug.resolution == "MOVED" && user.canmove) %]  
+           (bug.resolution == "MOVED" && bug.user.canmove) %]  
         <input type="radio" name="knob" value="reopen"> Reopen bug
         <br>
         [% knum = knum + 1 %]
@@ -574,7 +559,7 @@
       </b>
     </font>
 
-    [% IF user.canmove %]
+    [% IF bug.user.canmove %]
       &nbsp; <font size="+1"><b> | </b></font> &nbsp;
       <input type="submit" name="action" 
              value="[% Param("move-button-text") %]">
@@ -598,19 +583,11 @@
 <hr>
 
 [% PROCESS bug/comments.html.tmpl 
-   comments = bug.comments 
+   comments = bug.longdescs 
    mode = "edit"
  %]
   
 </form>
-<hr>
-
-[% PROCESS bug/navigate.html.tmpl %]
-
-<br>
-
-[% PROCESS global/footer.html.tmpl %]
-
 
 [%############################################################################%]
 [%# Block for dependencies                                                   #%]
@@ -620,7 +597,7 @@
   <th align="right">Bug [% bug.bug_id %] [%+ dep.title %]:</th>
   <td>
   [% FOREACH depbug = bug.${dep.fieldname} %]
-    [% GetBugLink(depbug, depbug) %][% " " %]
+    [% depbug FILTER bug_link(depbug) %][% " " %]
   [% END %]
   </td>
   <td>
@@ -638,7 +615,7 @@
   <td>
     <label for="[% selname %]" accesskey="[% accesskey %]">
       <select name="[% selname %]" id="[% selname %]">
-        [% FOREACH x = ${selname} %]
+        [% FOREACH x = bug.choices.${selname} %]
           <option value="[% x FILTER html %]"
             [% " selected" IF x == bug.${selname} %]>[% x FILTER html %]
           </option>
diff --git a/template/en/default/bug/process/next.html.tmpl b/template/en/default/bug/process/next.html.tmpl
index b7a2605e9..1eeb9f367 100644
--- a/template/en/default/bug/process/next.html.tmpl
+++ b/template/en/default/bug/process/next.html.tmpl
@@ -20,13 +20,22 @@
   #%]
 
 [%# INTERFACE:
-  # next_id : number; the ID of the next bug in the user's bug list.
+  # bug : Bug object; the next bug to show
   #%]
 
 <hr>
 
 <p>
   The next bug in your list is bug
-  <a href="show_bug.cgi?id=[% next_id %]">[% next_id %]</a>:
+  <a href="show_bug.cgi?id=[% bug.bug_id %]">[% bug.bug_id %]</a>:
 </p>
 
+[% PROCESS "bug/edit.html.tmpl" %]
+
+<hr>
+
+[% PROCESS bug/navigate.html.tmpl %]
+
+<br>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/bug/show.html.tmpl b/template/en/default/bug/show.html.tmpl
new file mode 100644
index 000000000..46f8c4674
--- /dev/null
+++ b/template/en/default/bug/show.html.tmpl
@@ -0,0 +1,46 @@
+<!-- 1.0@bugzilla.org -->
+[%# 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): Gervase Markham <gerv@gerv.net>
+  #                 Vaskin Kissoyan <vkissoyan@yahoo.com>
+  #                 Bradley Baetz <bbaetz@student.usyd.edu.au>
+  #%]
+
+[% filtered_desc = bug.short_desc FILTER html %]
+[% filtered_timestamp = bug.delta_ts FILTER time %]
+[% PROCESS global/header.html.tmpl 
+  title = "Bug $bug.bug_id - $bug.short_desc"
+  h1 = "Bugzilla Bug $bug.bug_id"
+  h2 = filtered_desc
+  h3 = "Last modified: $filtered_timestamp"
+  style_urls = [ "css/edit_bug.css" ]
+%]
+
+[% PROCESS bug/navigate.html.tmpl %]
+
+<hr>
+
+[% PROCESS bug/edit.html.tmpl %]
+
+<hr>
+
+[% PROCESS bug/navigate.html.tmpl %]
+
+<br>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl
index d23234adc..4b37ee4f1 100644
--- a/template/en/default/global/code-error.html.tmpl
+++ b/template/en/default/global/code-error.html.tmpl
@@ -48,6 +48,10 @@
     Attachment #[% attachid FILTER html %] ([% description FILTER html %]) 
     is already obsolete.
 
+  [% ELSIF error == "bug_error" %]
+    Trying to retrieve bug [% bug.bug_id %] returned the error
+    [% bug.error FILTER html %]
+
   [% ELSIF error == "cgi_error" %]
     [% title = "CGI Error" %]
     Bugzilla has had trouble interpreting your CGI request;
diff --git a/template/en/default/global/site-navigation.html.tmpl b/template/en/default/global/site-navigation.html.tmpl
index 118f356c0..77611b465 100644
--- a/template/en/default/global/site-navigation.html.tmpl
+++ b/template/en/default/global/site-navigation.html.tmpl
@@ -21,8 +21,8 @@
   #%]
 
 [%# INTERFACE:
-  # bug_list: list of integers. List of bugs numbers of current query (if any). 
-  # bug: integer. Number of current bug.
+  # bug_list: list of integers. List of bug numbers of current query (if any). 
+  # bug.bug_id: integer. Number of current bug (for navigation purposes)
   #%]
   
 [% IF NOT (user_agent.match("MSIE [1-6]") OR user_agent.match("Mozilla/4")) %]
diff --git a/template/en/default/pages/linked.html.tmpl b/template/en/default/pages/linked.html.tmpl
index c3d02885c..941c18cc7 100644
--- a/template/en/default/pages/linked.html.tmpl
+++ b/template/en/default/pages/linked.html.tmpl
@@ -30,7 +30,7 @@
 
 <p>
 <pre>
-[%- quoteUrls(form.text) FILTER html -%]
+[%- form.text FILTER quoteUrls FILTER html -%]
 </pre>
 </p>
 
@@ -45,7 +45,7 @@
 
 <p>
 <pre>
-[%- quoteUrls(form.text) -%]
+[%- form.text FILTER quoteUrls -%]
 </pre>
 </p>
 
-- 
2.24.1