Commit:    8ab9a1d1103117a6a185262ebeadba6c2595dcd4
Author:    Peter Kokot <peterko...@gmail.com>         Wed, 5 Dec 2018 14:51:46 
+0100
Parents:   487a0bc254f1bf1cc98df09fb33f7a6068f6620a
Branches:  master

Link:       
http://git.php.net/?p=web/bugs.git;a=commitdiff;h=8ab9a1d1103117a6a185262ebeadba6c2595dcd4

Log:
Refactor patches uploading

This patch moves patches uploading functionality from the outdated
HTTP_Upload package to a dedicated service class in the app.

Additional changes in this context:
- Functionality concerning retrieving patches data from database has
  been moved to a separate repository classes.
- Some missed bugs fixed when uploading patches and no developer info
  were recorded.
- Obsoleting patches functionality is now working again.
- Added a simple unit test.

Changed paths:
  M  README.md
  D  include/classes/bug_patchtracker.php
  M  include/functions.php
  A  src/Repository/ObsoletePatchRepository.php
  A  src/Repository/PatchRepository.php
  A  src/Utils/PatchTracker.php
  A  src/Utils/Uploader.php
  M  templates/addpatch.php
  A  tests/Utils/UploaderTest.php
  A  tests/fixtures/files/patch.txt
  M  www/bug.php
  M  www/patch-add.php
  M  www/patch-display.php
  M  www/report.php

diff --git a/README.md b/README.md
index 564aa47..7e353d0 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ composer install
 
 ```bash
 pear channel-update pear.php.net
-pear install --alldeps Text_Diff HTTP_Upload-1.0.0b4
+pear install --alldeps Text_Diff
 ```
 
 * Database:
diff --git a/include/classes/bug_patchtracker.php 
b/include/classes/bug_patchtracker.php
deleted file mode 100644
index 18c01d2..0000000
--- a/include/classes/bug_patchtracker.php
+++ /dev/null
@@ -1,364 +0,0 @@
-<?php
-
-require_once 'HTTP/Upload.php';
-
-class Bug_Patchtracker
-{
-       private $uploader;
-       private $dbh;
-
-       public function __construct()
-       {
-               if (!file_exists(BUG_PATCHTRACKER_TMPDIR)) {
-                       if (!@mkdir(BUG_PATCHTRACKER_TMPDIR)) {
-                               $this->uploader = false;
-                               $this->dbh = $GLOBALS['dbh'];
-                               return;
-                       }
-               }
-               $this->uploader = new HTTP_Upload('en');
-               $this->dbh = $GLOBALS['dbh'];
-       }
-
-       /**
-        * Return the directory in which patches should be stored
-        *
-        * @param int $bugid
-        * @param string $name name of this patch line
-        * @return string
-        */
-       private function patchDir($bugid, $name)
-       {
-               return BUG_PATCHTRACKER_TMPDIR . '/p' . $bugid . '/' . $name;
-       }
-       /**
-        * Create the directory in which patches for this bug ID will be stored
-        *
-        * @param int $bugid
-        */
-       private function setupPatchDir($bugid, $name)
-       {
-               if (file_exists($this->patchDir($bugid, $name))) {
-                       if (!is_dir($this->patchDir($bugid, $name))) {
-                               return PEAR::raiseError('Cannot create patch 
storage for Bug #' . $bugid .
-                                       ', storage directory exists and is not 
a directory');
-                       }
-                       return;
-               }
-               if (!file_exists(dirname($this->patchDir($bugid, $name)))) {
-                       // setup bug directory
-                       if (!@mkdir(dirname($this->patchDir($bugid, $name)))) {
-                               require_once 'PEAR.php';
-                               return PEAR::raiseError('Cannot create patch 
storage for Bug #' . $bugid);
-                       }
-               } elseif (!is_dir(dirname($this->patchDir($bugid, $name)))) {
-                       return PEAR::raiseError('Cannot create patch storage 
for Bug #' . $bugid .
-                               ', storage directory exists and is not a 
directory');
-               }
-               // setup patch directory
-               if (!@mkdir($this->patchDir($bugid, $name))) {
-                       require_once 'PEAR.php';
-                       return PEAR::raiseError('Cannot create patch storage 
for Bug #' . $bugid);
-               }
-       }
-
-       /**
-        * Retrieve a unique, ordered patch filename
-        *
-        * @param int $bugid
-        * @param string $patch
-        * @return array array(revision, patch file name)
-        */
-       private function newPatchFileName($bugid, $patch, $handle)
-       {
-               $id = time();
-               PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
-               $e = $this->dbh->prepare('INSERT INTO bugdb_patchtracker
-                       (bugdb_id, patch, revision, developer) VALUES(?, ?, ?, 
?)')->execute(
-                       [$bugid, $patch, $id, $handle]);
-               if (PEAR::isError($e)) {
-                       // try with another timestamp
-                       $id++;
-                       $e = $this->dbh->prepare('INSERT INTO bugdb_patchtracker
-                               (bugdb_id, patch, revision, developer) 
VALUES(?, ?, ?, ?)')->execute(
-                               [$bugid, $patch, $id, $handle]);
-               }
-               PEAR::popErrorHandling();
-               if (PEAR::isError($e)) {
-                       return PEAR::raiseError("Could not get unique patch 
file name for bug #{$bugid}, patch \"{$patch}\"");
-               }
-               return [$id, $this->getPatchFileName($id)];
-       }
-
-       /**
-        * Retrieve the name of the patch file on the system
-        *
-        * @param int $id revision ID
-        * @return string
-        */
-       private function getPatchFileName($id)
-       {
-               return 'p' . $id . '.patch.txt';
-       }
-
-       /**
-        * Retrieve the full path to a patch file
-        *
-        * @param int $bugid
-        * @param string $name
-        * @param int $revision
-        * @return string
-        */
-       public function getPatchFullpath($bugid, $name, $revision)
-       {
-               return $this->patchDir($bugid, 
$name).'/'.$this->getPatchFileName($revision);
-       }
-
-       /**
-        * Attach a patch to this bug
-        *
-        * @param int $bugid
-        * @param string $patch         uploaded patch filename form variable
-        * @param string $name          patch name (allows several patches to 
be versioned)
-        * @param string $handle        developer handle
-        * @param array $obsoletes      obsoleted patches
-        * @return int patch revision
-        */
-       public function attach($bugid, $patch, $name, $handle, $obsoletes)
-       {
-               if (!$this->uploader) {
-                       return PEAR::raiseError('Upload directory for patches 
could not be initialized');
-               }
-               if (!preg_match('/^[\w\-\.]+\z/', $name) || strlen($name) > 80) 
{
-                       return PEAR::raiseError("Invalid patch name 
\"".htmlspecialchars($name)."\"");
-               }
-               if (!is_array($obsoletes)) {
-                       return PEAR::raiseError('Invalid obsoleted patches');
-               }
-
-               $file = $this->uploader->getFiles($patch);
-               if (PEAR::isError($file)) {
-                       return $file;
-               }
-
-               if ($file->isValid()) {
-                       $newobsoletes = [];
-                       foreach ($obsoletes as $who) {
-                               if (!$who) {
-                                       continue; // remove (none)
-                               }
-                               $who = explode('#', $who);
-                               if (count($who) != 2) {
-                                       continue;
-                               }
-                               if (file_exists($this->getPatchFullpath($bugid, 
$who[0], $who[1]))) {
-                                       $newobsoletes[] = $who;
-                               }
-                       }
-                       if (PEAR::isError($e = $this->setupPatchDir($bugid, 
$name))) {
-                               return $e;
-                       }
-
-                       $res = $this->newPatchFileName($bugid, $name, $handle);
-                       if (PEAR::isError($res)) {
-                               return $res;
-                       }
-                       list($id, $fname) = $res;
-                       $file->setName($fname);
-                       $allowed_mime_types = [
-                               'application/x-txt',
-                               'text/plain',
-                               'text/x-diff',
-                               'text/x-patch',
-                               'text/x-c++',
-                               'text/x-c',
-                               'text/x-m4',
-                       ];
-
-                       // return mime type ala mimetype extension
-                       if (class_exists('finfo')) {
-                               $finfo = new finfo(FILEINFO_MIME);
-                               if (!$finfo) {
-                                       return PEAR::raiseError('Error: Opening 
fileinfo database failed');
-                               }
-
-                               // get mime-type for a specific file
-                               $mime = 
$finfo->file($file->getProp('tmp_name'));
-                               // get rid of the charset part
-                               $t      = explode(';', $mime);
-                               $mime = $t[0];
-                       }
-                       else // NOTE: I didn't have PHP 5.3 around with 
fileinfo enabled :)
-                       {
-                               $mime = 'text/plain';
-                       }
-                       if (!in_array($mime, $allowed_mime_types)) {
-                               $this->dbh->prepare('DELETE FROM 
bugdb_patchtracker
-                                       WHERE bugdb_id = ? and patch = ? and 
revision = ?')->execute(
-                                       [$bugid, $name, $id]);
-                               return PEAR::raiseError('Error: uploaded patch 
file must be text'
-                                       . ' file (save as e.g. "patch.txt" or 
"package.diff")'
-                                       . ' (detected "' . 
htmlspecialchars($mime) . '")'
-                               );
-                       }
-                       $tmpfile = $file->moveTo($this->patchDir($bugid, 
$name));
-                       if (PEAR::isError($tmpfile)) {
-                               $this->dbh->prepare('DELETE FROM 
bugdb_patchtracker
-                                       WHERE bugdb_id = ? and patch = ? and 
revision = ?')->execute(
-                                       [$bugid, $name, $id]);
-                               return $tmpfile;
-                       }
-                       if (!$file->getProp('size')) {
-                               $this->detach($bugid, $name, $id);
-                               return PEAR::raiseError('zero-length patches 
not allowed');
-                       }
-                       if ($file->getProp('size') > 102400) {
-                               $this->detach($bugid, $name, $id);
-                               return PEAR::raiseError('Patch files cannot be 
larger than 100k');
-                       }
-                       foreach ($newobsoletes as $obsolete) {
-                               $this->obsoletePatch($bugid, $name, $id, 
$obsolete[0], $obsolete[1]);
-                       }
-                       return $id;
-               } elseif ($file->isMissing()) {
-                       return PEAR::raiseError('Uploaded file is empty or 
nothing was uploaded.');
-               } elseif ($file->error()) {
-                       return PEAR::raiseError($file->errorMsg());
-               }
-               return PEAR::raiseError('Unable to attach patch (try renaming 
the file with .txt extension)');
-       }
-
-       /**
-        * Remove a patch revision from this bug
-        *
-        * @param int $bugid
-        * @param string $name
-        * @param int $revision
-        */
-       public function detach($bugid, $name, $revision)
-       {
-               $this->dbh->prepare('DELETE FROM bugdb_patchtracker
-                       WHERE bugdb_id = ? and patch = ? and revision = 
?')->execute(
-                       [$bugid, $name, $revision]);
-
-               @unlink($this->patchDir($bugid, 
$name).'/'.$this->getPatchFileName($revision));
-       }
-
-       /**
-        * Retrieve the actual contents of the patch
-        *
-        * @param int $bugid
-        * @param string $name
-        * @param int $revision
-        * @return string
-        */
-       public function getPatch($bugid, $name, $revision)
-       {
-               if ($this->dbh->prepare('
-                       SELECT bugdb_id
-                       FROM bugdb_patchtracker
-                       WHERE bugdb_id = ? AND patch = ? AND revision = 
?')->execute([$bugid, $name, $revision])->fetchOne()
-               ) {
-                       $contents = 
@file_get_contents($this->getPatchFullpath($bugid, $name, $revision));
-                       if (!$contents) {
-                               return PEAR::raiseError('Cannot retrieve patch 
revision "' . $revision . '" for patch "' . $name . '"');
-                       }
-                       return $contents;
-               }
-               return PEAR::raiseError('No such patch revision "' . $revision 
. '", or no such patch "' . $name . '"');
-       }
-
-       /**
-        * Retrieve a listing of all patches and their revisions
-        *
-        * @param int $bugid
-        * @return array
-        */
-       public function listPatches($bugid)
-       {
-               $query = '
-                       SELECT patch, revision, developer
-                       FROM bugdb_patchtracker
-                       WHERE bugdb_id = ?
-                       ORDER BY revision DESC
-               ';
-
-               return 
$this->dbh->prepare($query)->execute([$bugid])->fetchAll(PDO::FETCH_NUM, true, 
false, true);
-       }
-
-       /**
-        * Retrieve a listing of all patches and their revisions
-        *
-        * @param int $bugid
-        * @param string $patch
-        * @return array
-        */
-       public function listRevisions($bugid, $patch)
-       {
-               $query = '
-                       SELECT revision FROM bugdb_patchtracker
-                       WHERE bugdb_id = ? AND patch = ?
-                       ORDER BY revision DESC
-               ';
-               return $this->dbh->prepare($query)->execute([$bugid, 
$patch])->fetchAll(PDO::FETCH_NUM);
-       }
-
-       /**
-        * Retrieve the developer who uploaded this patch
-        *
-        * @param int $bugid
-        * @param string $patch
-        * @param int $revision
-        * @return string|array array if no revision is selected
-        */
-       public function getDeveloper($bugid, $patch, $revision = false)
-       {
-               if ($revision) {
-                       return $this->dbh->prepare('
-                               SELECT developer
-                               FROM bugdb_patchtracker
-                               WHERE bugdb_id = ? AND patch = ? AND revision = 
?
-                       ')->execute([$bugid, $patch, $revision])->fetchOne();
-               }
-               return $this->dbh->prepare('
-                       SELECT developer, revision
-                       FROM bugdb_patchtracker
-                       WHERE bugdb_id = ? AND patch = ? ORDER BY revision DESC
-               ')->execute([$bugid, $patch])->fetchAll(PDO::FETCH_ASSOC);
-       }
-
-       public function getObsoletingPatches($bugid, $patch, $revision)
-       {
-               return $this->dbh->prepare('
-                       SELECT bugdb_id, patch, revision
-                       FROM bugdb_obsoletes_patches
-                       WHERE   bugdb_id = ? AND obsolete_patch = ? AND 
obsolete_revision = ?
-               ')->execute([$bugid, $patch, 
$revision])->fetchAll(PDO::FETCH_ASSOC);
-       }
-
-       public function getObsoletePatches($bugid, $patch, $revision)
-       {
-               return $this->dbh->prepare('
-                       SELECT bugdb_id, obsolete_patch, obsolete_revision
-                       FROM bugdb_obsoletes_patches
-                       WHERE bugdb_id = ? AND patch = ? AND revision = ?
-               ')->execute([$bugid, $patch, 
$revision])->fetchAll(PDO::FETCH_ASSOC);
-       }
-
-       /**
-        * link to an obsolete patch from the new one
-        *
-        * @param int $bugid
-        * @param string $name better patch name
-        * @param int $revision better patch revision
-        * @param string $obsoletename
-        * @param int $obsoleterevision
-        */
-       private function obsoletePatch($bugid, $name, $revision, $obsoletename, 
$obsoleterevision)
-       {
-               $this->dbh->prepare('
-                       INSERT INTO bugdb_obsoletes_patches
-                       VALUES(?, ?, ?, ?, ?)
-               ')->execute([$bugid, $name, $revision, $obsoletename, 
$obsoleterevision]);
-       }
-}
diff --git a/include/functions.php b/include/functions.php
index c3a2c6a..8f35fe2 100644
--- a/include/functions.php
+++ b/include/functions.php
@@ -1148,7 +1148,7 @@ function format_date($ts = null, $format = 'Y-m-d H:i e')
        if (!$ts) {
                $ts = time();
        }
-       return gmdate($format, $ts - date('Z', $ts));
+       return gmdate($format, (int)$ts - date('Z', (int)$ts));
 }
 
 /**
diff --git a/src/Repository/ObsoletePatchRepository.php 
b/src/Repository/ObsoletePatchRepository.php
new file mode 100644
index 0000000..2335ec9
--- /dev/null
+++ b/src/Repository/ObsoletePatchRepository.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace App\Repository;
+
+use App\Database\Database;
+
+/**
+ * Repository for retrieving data from the bugdb_obsoletes_patches database 
table.
+ */
+class ObsoletePatchRepository
+{
+    /**
+     * Database handler.
+     * @var Database
+     */
+    private $dbh;
+
+    /**
+     * Class constructor.
+     */
+    public function __construct(Database $dbh)
+    {
+        $this->dbh = $dbh;
+    }
+
+    /**
+     * Retrieve patches that obsoleted given patch.
+     */
+    public function findObsoletingPatches(int $bugId, string $patch, int 
$revision): array
+    {
+        $sql = 'SELECT bugdb_id, patch, revision
+                FROM bugdb_obsoletes_patches
+                WHERE bugdb_id = ? AND obsolete_patch = ? AND 
obsolete_revision = ?
+        ';
+
+        return $this->dbh->prepare($sql)->execute([$bugId, $patch, 
$revision])->fetchAll();
+    }
+
+    /**
+     * Retrieve obsolete patches by bug, patch and revision.
+     */
+    public function findObsoletePatches(int $bugId, string $patch, int 
$revision): array
+    {
+        $sql = 'SELECT bugdb_id, obsolete_patch, obsolete_revision
+                FROM bugdb_obsoletes_patches
+                WHERE bugdb_id = ? AND patch = ? AND revision = ?
+        ';
+
+        return $this->dbh->prepare($sql)->execute([$bugId, $patch, 
$revision])->fetchAll();
+    }
+}
diff --git a/src/Repository/PatchRepository.php 
b/src/Repository/PatchRepository.php
new file mode 100644
index 0000000..a3a6d87
--- /dev/null
+++ b/src/Repository/PatchRepository.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace App\Repository;
+
+use App\Database\Database;
+
+/**
+ * Repository for retrieving data from the bugdb_patchtracker database table.
+ */
+class PatchRepository
+{
+    /**
+     * Database handler.
+     * @var Database
+     */
+    private $dbh;
+
+    /**
+     * Parent directory where patches are uploaded.
+     * @var string
+     */
+    private $uploadsDir;
+
+    /**
+     * Class constructor.
+     */
+    public function __construct(Database $dbh)
+    {
+        $this->dbh = $dbh;
+        $this->uploadsDir = BUG_PATCHTRACKER_TMPDIR;
+    }
+
+    /**
+     * Retrieve a list of all patches and their revisions by given bug id.
+     */
+    public function findAllByBugId(int $bugId): array
+    {
+        $sql = 'SELECT patch, revision, developer
+                FROM bugdb_patchtracker
+                WHERE bugdb_id = ?
+                ORDER BY revision DESC
+        ';
+
+        return $this->dbh->prepare($sql)->execute([$bugId])->fetchAll();
+    }
+
+    /**
+     * Retrieve the developer by patch.
+     */
+    public function findDeveloper(int $bugId, string $patch, int $revision): 
string
+    {
+        $sql = 'SELECT developer
+                FROM bugdb_patchtracker
+                WHERE bugdb_id = ? AND patch = ? AND revision = ?
+        ';
+
+        $arguments = [$bugId, $patch, $revision];
+
+        return $this->dbh->prepare($sql)->execute($arguments)->fetchOne();
+    }
+
+    /**
+     * Retrieve a list of all patches and their revisions.
+     */
+    public function findRevisions(int $bugId, string $patch): array
+    {
+        $sql = 'SELECT revision
+                FROM bugdb_patchtracker
+                WHERE bugdb_id = ? AND patch = ?
+                ORDER BY revision DESC
+        ';
+
+        return $this->dbh->prepare($sql)->execute([$bugId, 
$patch])->fetchAll();
+    }
+
+    /**
+     * Retrieve the actual contents of the patch.
+     */
+    public function getPatchContents(int $bugId, string $name, int $revision): 
string
+    {
+        $sql = 'SELECT bugdb_id
+                FROM bugdb_patchtracker
+                WHERE bugdb_id = ? AND patch = ? AND revision = ?
+        ';
+
+        if ($this->dbh->prepare($sql)->execute([$bugId, $name, 
$revision])->fetchOne()) {
+            $contents = @file_get_contents($this->getPatchPath($bugId, $name, 
$revision));
+
+            if (!$contents) {
+                throw new \Exception('Cannot retrieve patch revision 
"'.$revision.'" for patch "'.$name.'"');
+            }
+
+            return $contents;
+        }
+
+        throw new \Exception('No such patch revision "'.$revision.'", or no 
such patch "'.$name.'"');
+    }
+
+    /**
+     * Get absolute patch file name.
+     */
+    private function getPatchPath(int $bugId, string $name, int $revision): 
string
+    {
+        return 
$this->uploadsDir.'/p'.$bugId.'/'.$name.'/'.'p'.$revision.'.patch.txt';
+    }
+}
diff --git a/src/Utils/PatchTracker.php b/src/Utils/PatchTracker.php
new file mode 100644
index 0000000..127831a
--- /dev/null
+++ b/src/Utils/PatchTracker.php
@@ -0,0 +1,242 @@
+<?php
+
+namespace App\Utils;
+
+use App\Utils\Uploader;
+use App\Database\Database;
+
+/**
+ * Service for handling uploaded patches.
+ */
+class PatchTracker
+{
+    /**
+     * Database handler.
+     * @var Database
+     */
+    private $dbh;
+
+    /**
+     * File upload service.
+     * @var Uploader
+     */
+    private $uploader;
+
+    /**
+     * Parent directory where patches are uploaded.
+     * @var string
+     */
+    private $uploadsDir;
+
+    /**
+     * Maximum allowed patch file size.
+     */
+    const MAX_FILE_SIZE = 100 * 1024;
+
+    /**
+     * Valid media types (former MIME types) for the uploaded patch files.
+     */
+    const VALID_MEDIA_TYPES = [
+        'application/x-txt',
+        'text/plain',
+        'text/x-diff',
+        'text/x-patch',
+        'text/x-c++',
+        'text/x-c',
+        'text/x-m4',
+    ];
+
+    /**
+     * Class constructor.
+     */
+    public function __construct(Database $dbh, Uploader $uploader)
+    {
+        $this->dbh = $dbh;
+        $this->uploadsDir = BUG_PATCHTRACKER_TMPDIR;
+
+        $this->uploader = $uploader;
+        $this->uploader->setMaxFileSize(self::MAX_FILE_SIZE);
+        $this->uploader->setValidMediaTypes(self::VALID_MEDIA_TYPES);
+    }
+
+    /**
+     * Create a parent uploads directory for patches if it is missing.
+     */
+    private function createUploadsDir(): void
+    {
+        if (!file_exists($this->uploadsDir) && !@mkdir($this->uploadsDir)) {
+            throw new \Exception('Patches upload directory could not be 
created.');
+        }
+    }
+
+    /**
+     * Get the directory in which patches for given bug id should be stored.
+     */
+    private function getPatchDir(int $bugId, string $name): string
+    {
+        return $this->uploadsDir.'/p'.$bugId.'/'.$name;
+    }
+
+    /**
+     * Create the directory in which patches for the given bug id will be 
stored.
+     */
+    private function createPatchDir(int $bugId, string $name): void
+    {
+        $patchDir = $this->getPatchDir($bugId, $name);
+        $parentDir = dirname($patchDir);
+
+        // Check if patch directory already exists.
+        if (is_dir($patchDir)) {
+            return;
+        }
+
+        // Check if files with same names as directories already exist.
+        if (is_file($parentDir) || is_file($patchDir)) {
+            throw new \Exception('Cannot create patch storage for Bug 
#'.$bugId.', storage directory exists and is not a directory');
+        }
+
+        // Create parent directory
+        if (!file_exists($parentDir) && !@mkdir($parentDir)) {
+            throw new \Exception('Cannot create patch storage for Bug 
#'.$bugId);
+        }
+
+        // Create patch directory
+        if (!@mkdir($patchDir)) {
+            throw new \Exception('Cannot create patch storage for Bug 
#'.$bugId);
+        }
+    }
+
+    /**
+     * Retrieve a unique, ordered patch filename.
+     */
+    private function newPatchFileName(int $bugId, string $patch, string 
$developer): int
+    {
+        $revision = time();
+
+        $sql = 'INSERT INTO bugdb_patchtracker
+                (bugdb_id, patch, revision, developer) VALUES (?, ?, ?, ?)
+        ';
+
+        try {
+            $this->dbh->prepare($sql)->execute([$bugId, $patch, $revision, 
$developer]);
+        } catch (\Exception $e) {
+            // Try with another timestamp
+            try {
+                $revision++;
+                $this->dbh->prepare($sql)->execute([$bugId, $patch, $revision, 
$developer]);
+            } catch (\Exception $e) {
+                throw new \Exception('Could not get unique patch file name for 
bug #'.$bugId.', patch "'.$patch.'"');
+            }
+        }
+
+        return $revision;
+    }
+
+    /**
+     * Retrieve the name of the patch file on the system.
+     */
+    private function getPatchFileName(int $revision): string
+    {
+        return 'p'.$revision.'.patch.txt';
+    }
+
+    /**
+     * Retrieve the full path to a patch file.
+     */
+    public function getPatchFullpath(int $bugId, string $name, int $revision): 
string
+    {
+        return $this->getPatchDir($bugId, 
$name).'/'.$this->getPatchFileName($revision);
+    }
+
+    /**
+     * Attach a patch to this bug.
+     */
+    public function attach(int $bugId, string $patch, string $name, string 
$developer, array $obsoletes = []): int
+    {
+        $this->uploader->setDir($this->getPatchDir($bugId, $name));
+
+        if (!is_array($obsoletes)) {
+            throw new \Exception('Invalid obsoleted patches');
+        }
+
+        try {
+            $revision = $this->newPatchFileName($bugId, $name, $developer);
+            
$this->uploader->setDestinationFileName($this->getPatchFileName($revision));
+        } catch (\Exception $e) {
+            throw new \Exception($e->getMessage());
+        }
+
+        try {
+            $this->createUploadsDir();
+
+            $this->validatePatchName($name);
+
+            $this->createPatchDir($bugId, $name);
+
+            $this->uploader->upload($patch);
+        } catch (\Exception $e) {
+            $this->detach($bugId, $name, $revision);
+
+            throw new \Exception($e->getMessage());
+        }
+
+        $newObsoletes = [];
+        foreach ($obsoletes as $obsoletePatch) {
+            // The none option in form.
+            if (!$obsoletePatch) {
+                continue;
+            }
+
+            $obsoletePatch = explode('#', $obsoletePatch);
+
+            if (count($obsoletePatch) != 2) {
+                continue;
+            }
+
+            if (file_exists($this->getPatchFullpath($bugId, $obsoletePatch[0], 
$obsoletePatch[1]))) {
+                $newObsoletes[] = $obsoletePatch;
+            }
+        }
+
+        foreach ($newObsoletes as $obsolete) {
+            $this->obsoletePatch($bugId, $name, $revision, $obsolete[0], 
$obsolete[1]);
+        }
+
+        return $revision;
+    }
+
+    /**
+     * Validate patch name.
+     */
+    private function validatePatchName(string $name): void
+    {
+        if (!preg_match('/^[\w\-\.]+\z/', $name) || strlen($name) > 80) {
+            throw new \Exception('Invalid patch name 
"'.htmlspecialchars($name, ENT_QUOTES).'"');
+        }
+    }
+
+    /**
+     * Remove a patch revision from this bug.
+     */
+    private function detach(int $bugId, string $name, int $revision): void
+    {
+        $sql = 'DELETE FROM bugdb_patchtracker
+                WHERE bugdb_id = ? AND patch = ? AND revision = ?
+        ';
+
+        $this->dbh->prepare($sql)->execute([$bugId, $name, $revision]);
+
+        @unlink($this->getPatchFullpath($bugId, $name, $revision));
+    }
+
+    /**
+     * Make patch obsolete by new patch. This create a link to an obsolete 
patch
+     * from the new one.
+     */
+    private function obsoletePatch(int $bugId, string $name, int $revision, 
string $obsoleteName, int $obsoleteRevision): void
+    {
+        $sql = 'INSERT INTO bugdb_obsoletes_patches VALUES (?, ?, ?, ?, ?)';
+
+        $this->dbh->prepare($sql)->execute([$bugId, $name, $revision, 
$obsoleteName, $obsoleteRevision]);
+    }
+}
diff --git a/src/Utils/Uploader.php b/src/Utils/Uploader.php
new file mode 100644
index 0000000..6591b78
--- /dev/null
+++ b/src/Utils/Uploader.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace App\Utils;
+
+/**
+ * A basic upload service class for uploading files via HTML forms.
+ */
+class Uploader
+{
+    /**
+     * Maximum allowed file size in bytes.
+     * @var int
+     */
+    private $maxFileSize = 2 * 1024 * 1024;
+
+    /**
+     * Valid file extension.
+     * @var string
+     */
+    private $validExtension;
+
+    /**
+     * Valid media types.
+     * @var array
+     */
+    private $validMediaTypes;
+
+    /**
+     * Destination directory.
+     * @var string
+     */
+    private $dir;
+
+    /**
+     * Destination file name.
+     * @var string
+     */
+    private $destinationFileName;
+
+    /**
+     * Set the maximum allowed file size in bytes.
+     */
+    public function setMaxFileSize(int $maxFileSize): void
+    {
+        $this->maxFileSize = $maxFileSize;
+    }
+
+    /**
+     * Set allowed file extension without leading dot. For example, 'tgz'.
+     */
+    public function setValidExtension(string $validExtension): void
+    {
+        $this->validExtension = $validExtension;
+    }
+
+    /**
+     * Set array of valid media types.
+     */
+    public function setValidMediaTypes(array $validMediaTypes): void
+    {
+        $this->validMediaTypes = $validMediaTypes;
+    }
+
+    /**
+     * Set destination directory.
+     */
+    public function setDir(string $dir): void
+    {
+        $this->dir = $dir;
+    }
+
+    /**
+     * Set the destination file name.
+     */
+    public function setDestinationFileName(string $destinationFileName): void
+    {
+        $this->destinationFileName = $destinationFileName;
+    }
+
+    /**
+     * Upload file.
+     */
+    public function upload(string $key): string
+    {
+        $files = isset($_FILES[$key]) ? $_FILES[$key] : [];
+
+        // Check if uploaded file size exceeds the ini post_max_size directive.
+        if(
+            empty($_FILES)
+            && empty($_POST)
+            && isset($_SERVER['REQUEST_METHOD'])
+            && strtolower($_SERVER['REQUEST_METHOD']) === 'post'
+        ) {
+            $max = ini_get('post_max_size');
+            throw new \Exception('Error on upload: Exceeded POST content 
length server limit of '.$max);
+        }
+
+        // Some other upload error happened
+        if (empty($files) || $files['error'] !== UPLOAD_ERR_OK) {
+            throw new \Exception('Error on upload: Something went wrong. Error 
code: '.$files['error']);
+        }
+
+        // Be sure we're dealing with an upload
+        if ($this->isUploadedFile($files['tmp_name']) === false) {
+            throw new \Exception('Error on upload: Invalid file definition');
+        }
+
+        // Check file extension
+        $uploadedName = $files['name'];
+        $ext = $this->getFileExtension($uploadedName);
+        if (isset($this->validExtension) && $ext !== $this->validExtension) {
+            throw new \Exception('Error on upload: Invalid file extension. 
Should be .'.$this->validExtension);
+        }
+
+        // Check file size
+        if ($files['size'] > $this->maxFileSize) {
+            throw new \Exception('Error on upload: Exceeded file size limit 
'.$this->maxFileSize.' bytes');
+        }
+
+        // Check zero length file size
+        if (!$files['size']) {
+            throw new \Exception('Error on upload: Zero-length patches are not 
allowed');
+        }
+
+        // Check media type
+        $type = $this->getMediaType($files['tmp_name']);
+        if (isset($this->validMediaTypes) && !in_array($type, 
$this->validMediaTypes)) {
+            throw new \Exception('Error: Uploaded patch file must be text file
+                (save as e.g. "patch.txt" or "package.diff")
+                (detected "'.htmlspecialchars($type, ENT_QUOTES).'")'
+            );
+        }
+
+        // Rename the uploaded file
+        $destination = $this->dir.'/'.$this->destinationFileName;
+
+        // Move uploaded file to final destination
+        if (!$this->moveUploadedFile($files['tmp_name'], $destination)) {
+            throw new \Exception('Error on upload: Something went wrong');
+        }
+
+        return $destination;
+    }
+
+    /**
+     * Checks if given file has been uploaded via POST method. This is wrapped
+     * into a separate method for convenience of testing it via phpunit and 
using
+     * a mock.
+     */
+    protected function isUploadedFile(string $file): bool
+    {
+        return is_uploaded_file($file);
+    }
+
+    /**
+     * Move uploaded file to destination. This method is wrapping PHP function
+     * to allow testing with PHPUnit and creating a mock object.
+     */
+    protected function moveUploadedFile(string $source, string $destination): 
bool
+    {
+        return move_uploaded_file($source, $destination);
+    }
+
+    /**
+     * Rename file to a unique name.
+     */
+    protected function renameFile(string $filename): ?string
+    {
+        $ext = $this->getFileExtension($filename);
+
+        $rand = uniqid(rand());
+
+        $i = 0;
+        while (true) {
+            $newName = $rand.$i.'.'.$ext;
+
+            if (!file_exists($this->dir.'/'.$newName)) {
+                return $newName;
+            }
+
+            $i++;
+        }
+    }
+
+    /**
+     * Returns file extension without a leading dot.
+     */
+    protected function getFileExtension(string $filename): string
+    {
+        return strtolower(substr($filename, strripos($filename, '.') + 1));
+    }
+
+    /**
+     * Guess file media type (formerly known as MIME type) using the fileinfo
+     * extension. If fileinfo extension is not installed fallback to plain text
+     * type.
+     */
+    protected function getMediaType(string $file): string
+    {
+        // If fileinfo extension is not available it defaults to text/plain.
+        if (!class_exists('finfo')) {
+            return 'text/plain';
+        }
+
+        $finfo = new \finfo(FILEINFO_MIME);
+
+        if (!$finfo) {
+            throw new \Exception('Error: Opening fileinfo database failed.');
+        }
+
+        // Get type for a specific file
+        $type = $finfo->file($file);
+
+        // Remove the charset part
+        $mediaType = explode(';', $type);
+
+        return isset($mediaType[0]) ? $mediaType[0] : 'text/plain';
+    }
+}
diff --git a/templates/addpatch.php b/templates/addpatch.php
index 5285789..faad8c8 100644
--- a/templates/addpatch.php
+++ b/templates/addpatch.php
@@ -61,15 +61,13 @@ if (!$logged_in) {
   <td class="form-input">
    <select name="obsoleted[]" multiple="true" size="5">
     <option value="0">(none)</option>
-<?php
-   foreach ($patches as $patchname => $patch2) {
-       foreach ($patch2 as $patch) {
-           echo '<option value="', htmlspecialchars($patchname . '#' . 
$patch[0]),
-                '">', htmlspecialchars($patchname), ', Revision ',
-                format_date($patch[0]), ' (', $patch[1], ')</option>';
-       }
-   }
-?>
+    <?php foreach ($patches as $patch): ?>
+        <option value="<?= 
htmlspecialchars($patch['patch'].'#'.$patch['revision'], ENT_QUOTES); ?>">
+            <?= htmlspecialchars($patch['patch'], ENT_QUOTES); ?>,
+            Revision <?= format_date($patch['revision']); ?>
+            <?= $patch['developer'] ? 
'('.spam_protect(htmlspecialchars($patch['developer'], ENT_QUOTES)).')' : ''; ?>
+        </option>
+    <?php endforeach; ?>
    </select>
   </td>
  </tr>
diff --git a/tests/Utils/UploaderTest.php b/tests/Utils/UploaderTest.php
new file mode 100644
index 0000000..982cc7f
--- /dev/null
+++ b/tests/Utils/UploaderTest.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace App\Tests\Utils;
+
+use PHPUnit\Framework\TestCase;
+use App\Utils\Uploader;
+
+class UploaderTest extends TestCase
+{
+    private $fixturesDirectory = __DIR__.'/../fixtures/files';
+
+    /**
+     * @dataProvider filesProvider
+     */
+    public function testUpload($validExtension, $file)
+    {
+        $_FILES = [];
+        $_FILES['uploaded'] = $file;
+
+        $uploader = $this->getMockBuilder(Uploader::class)
+            ->setMethods(['isUploadedFile', 'moveUploadedFile'])
+            ->getMock();
+
+        $uploader->expects($this->once())
+                 ->method('isUploadedFile')
+                 ->will($this->returnValue(true));
+
+        $uploader->expects($this->once())
+                 ->method('moveUploadedFile')
+                 ->will($this->returnValue(true));
+
+        $uploader->setMaxFileSize(100 * 1024);
+        $uploader->setValidExtension($validExtension);
+        $uploader->setDir(__DIR__.'/../../var/uploads');
+        $tmpFile = $uploader->upload('uploaded');
+
+        $this->assertNotNull($tmpFile);
+    }
+
+    public function filesProvider()
+    {
+        return [
+            [
+                'txt',
+                [
+                    'name' => 'patch.txt',
+                    'tmp_name' => $this->fixturesDirectory.'/patch.txt',
+                    'size' => filesize($this->fixturesDirectory.'/patch.txt'),
+                    'error' => UPLOAD_ERR_OK,
+                ]
+            ],
+        ];
+    }
+}
diff --git a/tests/fixtures/files/patch.txt b/tests/fixtures/files/patch.txt
new file mode 100644
index 0000000..d675fa4
--- /dev/null
+++ b/tests/fixtures/files/patch.txt
@@ -0,0 +1 @@
+foo bar
diff --git a/www/bug.php b/www/bug.php
index c676d13..bb87dc7 100644
--- a/www/bug.php
+++ b/www/bug.php
@@ -1,6 +1,8 @@
 <?php
 /* User interface for viewing and editing bug details */
 
+use App\Repository\ObsoletePatchRepository;
+use App\Repository\PatchRepository;
 use App\Utils\Captcha;
 use App\Repository\PullRequestRepository;
 
@@ -10,6 +12,9 @@ require_once '../include/prepend.php';
 // Start session
 session_start();
 
+$obsoletePatchRepository = new ObsoletePatchRepository($dbh);
+$patchRepository = new PatchRepository($dbh);
+
 define('SPAM_REJECT_MESSAGE', 'Your comment looks like SPAM by its content. 
Please consider rewording.');
 $email = null;
 
@@ -1059,9 +1064,7 @@ if ($bug['ldesc']) {
 
 // Display patches
 if ($show_bug_info && $bug_id != 'PREVIEW' && $bug['status'] !== 'Spam') {
-       require_once "{$ROOT_DIR}/include/classes/bug_patchtracker.php";
-       $patches = new Bug_Patchtracker;
-       $p = $patches->listPatches($bug_id);
+       $p = $patchRepository->findAllByBugId($bug_id);
        $revs = [];
        echo "<h2>Patches</h2>\n";
 
@@ -1071,7 +1074,7 @@ if ($show_bug_info && $bug_id != 'PREVIEW' && 
$bug['status'] !== 'Spam') {
 
        foreach ($revs as $name => $revisions)
        {
-               $obsolete = $patches->getObsoletingPatches($bug_id, $name, 
$revisions[0][0]);
+               $obsolete = 
$obsoletePatchRepository->findObsoletingPatches($bug_id, $name, 
$revisions[0][0]);
                $style = !empty($obsolete) ? ' style="background-color: yellow; 
text-decoration: line-through;" ' : '';
                $url_name = urlencode($name);
                $clean_name = clean($name);
@@ -1080,7 +1083,7 @@ if ($show_bug_info && $bug_id != 'PREVIEW' && 
$bug['status'] !== 'Spam') {
 
                echo <<< OUTPUT
 <a 
href="patch-display.php?bug_id={$bug_id}&amp;patch={$url_name}&amp;revision=latest"
 {$style}>{$clean_name}</a>
-(last revision {$formatted_date}) by {$submitter})
+(last revision {$formatted_date} by {$submitter})
 <br>
 OUTPUT;
        }
diff --git a/www/patch-add.php b/www/patch-add.php
index 0b1db80..763a011 100644
--- a/www/patch-add.php
+++ b/www/patch-add.php
@@ -1,10 +1,17 @@
 <?php
 
+use App\Repository\PatchRepository;
 use App\Utils\Captcha;
+use App\Utils\PatchTracker;
+use App\Utils\Uploader;
 
 // Obtain common includes
 require_once '../include/prepend.php';
 
+$uploader = new Uploader();
+$patchTracker = new PatchTracker($dbh, $uploader);
+$patchRepository = new PatchRepository($dbh);
+
 session_start();
 
 // Authenticate
@@ -50,9 +57,6 @@ if (!$show_bug_info) {
        exit;
 }
 
-require_once "{$ROOT_DIR}/include/classes/bug_patchtracker.php";
-$patchinfo = new Bug_Patchtracker;
-
 $patch_name = (!empty($_GET['patchname']) && is_string($_GET['patchname'])) ? 
$_GET['patchname'] : '';
 $patch_name = (!empty($_POST['name']) && is_string($_POST['name'])) ? 
$_POST['name'] : $patch_name;
 $patch_name_url = urlencode($patch_name);
@@ -64,7 +68,7 @@ if (isset($_POST['addpatch'])) {
 
        // Check that patch name is given (required always)
        if (empty($patch_name)) {
-               $patches = $patchinfo->listPatches($bug_id);
+               $patches = $patchRepository->findAllByBugId($bug_id);
                $errors[] = 'No patch name entered';
                include "{$ROOT_DIR}/templates/addpatch.php";
                exit;
@@ -90,24 +94,23 @@ if (isset($_POST['addpatch'])) {
                        }
 
                        if (count($errors)) {
-                               throw new Exception('');
+                               throw new \Exception('');
                        }
 
-                       PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
-                       $e = $patchinfo->attach($bug_id, 'patch', $patch_name, 
$email, $_POST['obsoleted']);
-                       PEAR::popErrorHandling();
-
-                       if (PEAR::isError($e)) {
-                               $patches = $patchinfo->listPatches($bug_id);
+                       try {
+                               $revision = $patchTracker->attach($bug_id, 
'patch', $patch_name, $email, $_POST['obsoleted']);
+                       } catch (\Exception $e) {
+                               $patches = 
$patchRepository->findAllByBugId($bug_id);
                                $errors[] = $e->getMessage();
-                               $errors[] = 'Could not attach patch "' . 
htmlspecialchars($patch_name) . '" to Bug #' . $bug_id;
+                               $errors[] = 'Could not attach patch 
"'.htmlspecialchars($patch_name).'" to Bug #'.$bug_id;
                                include "{$ROOT_DIR}/templates/addpatch.php";
+
                                exit;
                        }
 
-                       
redirect("patch-display.php?bug={$bug_id}&patch={$patch_name_url}&revision={$e}");
-               } catch (Exception $e) {
-                       $patches = $patchinfo->listPatches($bug_id);
+                       
redirect("patch-display.php?bug={$bug_id}&patch={$patch_name_url}&revision={$revision}");
+               } catch (\Exception $e) {
+                       $patches = $patchRepository->findAllByBugId($bug_id);
                        include "{$ROOT_DIR}/templates/addpatch.php";
                        exit;
                }
@@ -115,27 +118,27 @@ if (isset($_POST['addpatch'])) {
                $email = $auth_user->email;
        }
 
-       PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
-       $e = $patchinfo->attach($bug_id, 'patch', $patch_name, 
$auth_user->email, $_POST['obsoleted']);
-       PEAR::popErrorHandling();
-       if (PEAR::isError($e)) {
-               $patches = $patchinfo->listPatches($bug_id);
-               $errors = [$e->getMessage(),
-                       'Could not attach patch "' .
-                       htmlspecialchars($patch_name) .
-                       '" to Bug #' . $bug_id];
+       try {
+               $revision = $patchTracker->attach($bug_id, 'patch', 
$patch_name, $auth_user->email, $_POST['obsoleted']);
+       } catch (\Exception $e) {
+               $patches = $patchRepository->findAllByBugId($bug_id);
+               $errors = [
+                       $e->getMessage(),
+                       'Could not attach patch 
"'.htmlspecialchars($patch_name, ENT_QUOTES).'" to Bug #'.$bug_id
+               ];
                include "{$ROOT_DIR}/templates/addpatch.php";
+
                exit;
        }
 
        // Add a comment to the bug report.
-       $patch_url = 
"{$site_method}://{$site_url}{$basedir}/patch-display.php?bug={$bug_id}&patch={$patch_name_url}&revision={$e}";
+       $patch_url = 
"{$site_method}://{$site_url}{$basedir}/patch-display.php?bug={$bug_id}&patch={$patch_name_url}&revision={$revision}";
 
        $text = <<<TXT
 The following patch has been added/updated:
 
 Patch Name: {$patch_name}
-Revision:   {$e}
+Revision:   {$revision}
 URL:        {$patch_url}
 TXT;
 
@@ -144,13 +147,13 @@ TXT;
        // Send emails
        mail_bug_updates($buginfo, $buginfo, $auth_user->email, $text, 4, 
$bug_id);
 
-       $patches = $patchinfo->listPatches($bug_id);
+       $patches = $patchRepository->findAllByBugId($bug_id);
        $errors = [];
        include "{$ROOT_DIR}/templates/patchadded.php";
        exit;
 }
 
 $email = isset($_GET['email']) ? $_GET['email'] : '';
-$patches = $patchinfo->listPatches($bug_id);
+$patches = $patchRepository->findAllByBugId($bug_id);
 
 include "{$ROOT_DIR}/templates/addpatch.php";
diff --git a/www/patch-display.php b/www/patch-display.php
index 831e340..bcf82f5 100644
--- a/www/patch-display.php
+++ b/www/patch-display.php
@@ -1,10 +1,20 @@
 <?php
 
+use App\Repository\ObsoletePatchRepository;
+use App\Repository\PatchRepository;
+use App\Utils\PatchTracker;
+use App\Utils\Uploader;
+
 session_start();
 
 // Obtain common includes
 require_once '../include/prepend.php';
 
+$obsoletePatchRepository = new ObsoletePatchRepository($dbh);
+$patchRepository = new PatchRepository($dbh);
+$uploader = new Uploader();
+$patchTracker = new PatchTracker($dbh, $uploader);
+
 // Authenticate
 bugs_authenticate($user, $pw, $logged_in, $user_flags);
 
@@ -30,9 +40,6 @@ if (empty($bug_id)) {
        $bug_id = (int) $_GET['bug_id'];
 }
 
-require "{$ROOT_DIR}/include/classes/bug_patchtracker.php";
-$patchinfo = new Bug_Patchtracker;
-
 if (!($buginfo = bugs_get_bug($bug_id))) {
        response_header('Error :: invalid bug selected');
        display_bug_error("Invalid bug #{$bug_id} selected");
@@ -51,13 +58,13 @@ $pseudo_pkgs = get_pseudo_packages(false);
 
 if (isset($patch_name) && isset($revision)) {
        if ($revision == 'latest') {
-               $revisions = $patchinfo->listRevisions($buginfo['id'], 
$patch_name);
+               $revisions = $patchRepository->findRevisions($buginfo['id'], 
$patch_name);
                if (isset($revisions[0])) {
                        $revision = $revisions[0]['revision'];
                }
        }
 
-       $path = $patchinfo->getPatchFullpath($bug_id, $patch_name, $revision);
+       $path = $patchTracker->getPatchFullpath($bug_id, $patch_name, 
$revision);
        if (!file_exists($path)) {
                response_header('Error :: no such patch/revision');
                display_bug_error('Invalid patch/revision specified');
@@ -73,9 +80,10 @@ if (isset($patch_name) && isset($revision)) {
                readfile($path);
                exit;
        }
-       $patchcontents = $patchinfo->getPatch($buginfo['id'], $patch_name, 
$revision);
 
-       if (PEAR::isError($patchcontents)) {
+       try {
+               $patchcontents = 
$patchRepository->getPatchContents($buginfo['id'], $patch_name, $revision);
+       } catch (\Exception $e) {
                response_header('Error :: Cannot retrieve patch');
                display_bug_error('Internal error: Invalid patch/revision 
specified (is in database, but not in filesystem)');
                response_footer();
@@ -83,17 +91,17 @@ if (isset($patch_name) && isset($revision)) {
        }
 
        $package_name = $buginfo['package_name'];
-       $handle = $patchinfo->getDeveloper($bug_id, $patch_name, $revision);
-       $obsoletedby = $patchinfo->getObsoletingPatches($bug_id, $patch_name, 
$revision);
-       $obsoletes = $patchinfo->getObsoletePatches($bug_id, $patch_name, 
$revision);
-       $patches = $patchinfo->listPatches($bug_id);
-       $revisions = $patchinfo->listRevisions($bug_id, $patch_name);
+       $handle = $patchRepository->findDeveloper($bug_id, $patch_name, 
$revision);
+       $obsoletedby = $obsoletePatchRepository->findObsoletingPatches($bug_id, 
$patch_name, $revision);
+       $obsoletes = $obsoletePatchRepository->findObsoletePatches($bug_id, 
$patch_name, $revision);
+       $patches = $patchRepository->findAllByBugId($bug_id);
+       $revisions = $patchRepository->findRevisions($bug_id, $patch_name);
 
        response_header("Bug #{$bug_id} :: Patches");
        include "{$ROOT_DIR}/templates/listpatches.php";
 
        if (isset($_GET['diff']) && $_GET['diff'] && isset($_GET['old']) && 
is_numeric($_GET['old'])) {
-               $old = $patchinfo->getPatchFullpath($bug_id, $patch_name, 
$_GET['old']);
+               $old = $patchTracker->getPatchFullpath($bug_id, $patch_name, 
$_GET['old']);
                $new = $path;
                if (!realpath($old) || !realpath($new)) {
                        response_header('Error :: Cannot retrieve patch');
@@ -116,7 +124,7 @@ if (isset($patch_name) && isset($revision)) {
        exit;
 }
 
-$patches = $patchinfo->listPatches($bug_id);
+$patches = $patchTracker->listPatches($bug_id);
 response_header("Bug #{$bug_id} :: Patches");
 include "{$ROOT_DIR}/templates/listpatches.php";
 response_footer();
diff --git a/www/report.php b/www/report.php
index c2a52f4..9cd6414 100644
--- a/www/report.php
+++ b/www/report.php
@@ -1,6 +1,8 @@
 <?php
 
 use App\Utils\Captcha;
+use App\Utils\PatchTracker;
+use App\Utils\Uploader;
 
 // Obtain common includes
 require_once '../include/prepend.php';
@@ -225,12 +227,13 @@ OUTPUT;
 
                        $redirectToPatchAdd = false;
                        if (!empty($_POST['in']['patchname']) && 
$_POST['in']['patchname']) {
-                               require_once 
"{$ROOT_DIR}/include/classes/bug_patchtracker.php";
-                               $tracker = new Bug_Patchtracker;
-                               
PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
-                               $patchrevision = $tracker->attach($cid, 
'patchfile', $_POST['in']['patchname'], $_POST['in']['handle'], []);
-                               PEAR::staticPopErrorHandling();
-                               if (PEAR::isError($patchrevision)) {
+                               $uploader = new Uploader();
+                               $tracker = new PatchTracker($dbh, $uploader);
+
+                               try {
+                                       $developer = 
!empty($_POST['in']['handle']) ? $_POST['in']['handle'] : $_POST['in']['email'];
+                                       $patchrevision = $tracker->attach($cid, 
'patchfile', $_POST['in']['patchname'], $developer, []);
+                               } catch (\Exception $e) {
                                        $redirectToPatchAdd = true;
                                }
                        }
-- 
PHP Webmaster List Mailing List (http://www.php.net/)
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to