Commit: 8ab9a1d1103117a6a185262ebeadba6c2595dcd4
Author: Peter Kokot <[email protected]> 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}&patch={$url_name}&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