Modified: pear2/BugManager/trunk/data/bugmanager.sql =================================================================== --- pear2/BugManager/trunk/data/bugmanager.sql 2009-08-13 03:32:47 UTC (rev 287196) +++ pear2/BugManager/trunk/data/bugmanager.sql 2009-08-13 05:02:25 UTC (rev 287197) @@ -6,7 +6,7 @@ id int(8) NOT NULL PRIMARY KEY auto_increment, package_name varchar(80) default NULL, bug_type varchar(32) NOT NULL default 'Bug', - handle varchar(20) NOT NULL default '', + handle varchar(32) NOT NULL default '', email varchar(40) NOT NULL default '', reporter_name varchar(80) default '', sdesc varchar(80) NOT NULL default '', @@ -31,7 +31,7 @@ id int(8) NOT NULL PRIMARY KEY auto_increment, bug_id int(8) NOT NULL default '0', email varchar(40) NOT NULL default '', - handle varchar(20) NOT NULL default '', + handle varchar(32) NOT NULL default '', reporter_name varchar(80) default '', ts datetime NOT NULL default '0000-00-00 00:00:00', comment text NOT NULL @@ -82,10 +82,10 @@ CREATE TABLE bugmanager_patchtracker ( bug_id int(8) NOT NULL, - patch varchar(40) NOT NULL, + name varchar(40) NOT NULL, revision int(8) NOT NULL, - developer varchar(20) NOT NULL, - PRIMARY KEY (bug_id, patch, revision) + handle varchar(32) NOT NULL, + PRIMARY KEY (bug_id, name, revision) ); CREATE TABLE bugmanager_obsoletes_patches ( Modified: pear2/BugManager/trunk/src/BugManager/Attachment.php =================================================================== --- pear2/BugManager/trunk/src/BugManager/Attachment.php 2009-08-13 03:32:47 UTC (rev 287196) +++ pear2/BugManager/trunk/src/BugManager/Attachment.php 2009-08-13 05:02:25 UTC (rev 287197) @@ -26,8 +26,197 @@ namespace pear2\BugManager; class Attachment { + static protected $patchDir = false; + protected $info = array( + 'obsolete' => array(), + 'revision' => null, + 'name' => null, + 'mimetype' => null, + ); + protected $bug; + protected $comment; + protected $FILES; + + static function setPatchDirectory($dir, $create = true) + { + if (!file_exists($dir) && !$create) { + throw new LogicException('patch/attachment directory does not exist'); + } + if (!@mkdir($dir)) { + throw new LogicException('cannot create patch/attachment directory'); + } + self::$patchDir = $dir; + } + + function __construct(Issue $bug, Comment $comment, $name, $FILES) + { + if (!self::$patchDir) { + throw new BadMethodCallException('use pear2\BugManager\Attachment::setPatchDirectory' . + ' prior to instantiating an attachment'); + } + $this->info['name'] = $name; + $this->bug = $bug; + $this->FILES = $FILES; + } + + /** + * Return the directory in which attachments/patches should be stored + * + * @return string + */ + protected function patchDir() + { + return self::$patchDir . '/p' . $this->bug . '/' . $this->info['name']; + } + + /** + * Create the directory in which patches for this bug ID will be stored + * + * @param int $bugid + */ + protected function setupPatchDir() + { + if (file_exists(self::patchDir()) && !is_dir(self::patchDir())) { + throw new UnexpectedValueException('Cannot create attachment storage for Issue #' . + $this->bug->id . + ', storage directory exists and is not a directory'); + } elseif (!(@mkdir(self::patchDir(), 0755, true))) { + throw new UnexpectedValueException('Cannot create attachment storage directory for Issue #' . + $this->bug->id); + } + } + + /** + * Retrieve the full path to a patch file + * + * @param string $name + * @param int $revision + * @return string + */ + function getPatchFullpath($name = null, $revision = null) + { + if (null === $name) { + $name = $this->info['name']; + } + if (null === $revision) { + $revision = $this->info['revision']; + } + return $this->patchDir($this->bug->id, $name) . + DIRECTORY_SEPARATOR . 'p' . $revision . '.patch.txt'; + } + + /** + * Retrieve a downloaded patch + * @param string the name of the form field to retrieve + */ + function attach($formfield) + { + if (null == $this->info['name']) { + throw new BadMethodCallException('Cannot attach, set name first'); + } + $newobsoletes = array(); + foreach ($this->info['obsoletes'] as $who) { + if (!$who) { + continue; // remove (none) + } + $who = explode('#', $who); + if (count($who) != 2) { + continue; + } + if (file_exists($this->getPatchFullpath($who[0], $who[1]))) { + $newobsoletes[] = $who; + } + } + $this->setupPatchDir(); + if (!isset($this->FILES[$formfield])) { + throw new UnexpectedValueException('Could not find form field "' . $formfield . + '" in the specified $_FILES array'); + } + if ($this->FILES[$formfield]['error']) { + // upload failed + switch ($this->FILES[$formfield]['error']) { + case UPLOAD_ERR_FORM_SIZE : + case UPLOAD_ERR_INI_SIZE : + throw new ValidationException('Attachment is too big'); + case UPLOAD_ERR_CANT_WRITE : + throw new BadMethodCallException('Internal error: failed to save attachment'); + case UPLOAD_ERR_EXTENSION : + throw new BadMethodCallException('Internal error: upload halted by extension'); + case UPLOAD_ERR_NO_FILE : + throw new ValidationException('Nothing was attached, try again'); + case UPLOAD_ERR_NO_TMP_DIR : + throw new BadMethodCallException('Internal error: no temporary folder available'); + case UPLOAD_ERR_PARTIAL : + throw new BadMethodCallException('Internal error: attachment only partially uploaded'); + } + } + if (!$this->FILES[$formfield]['size']) { + throw new ValidationException('Attachment has size zero, ignoring'); + } + $file = $this->FILES[$formfield]['tmp_name']; + $allowed_mime_types = array( + 'application/x-txt', + 'text/plain', + 'text/x-diff', + 'text/x-patch', + 'text/x-c++', + 'text/x-c', + 'application/zip', + 'application/x-tar', + ); + + $finfo = new finfo(FILEINFO_MIME_ENCODING); + + // get mime-type for a specific file + $mime = $finfo->file($file); + + if (!in_array($mime, $allowed_mime_types)) { + throw new ValidationException('Error: uploaded attachment must be a text' + . ' file (save as e.g. "patch.txt" or "package.diff") or a tar/zip archive' + . ' (detected "' . htmlspecialchars($mime) . '")' + ); + } + if ($this->FILES[$formfield]['size'] > 102400) { + throw new ValidationException('Attachment files cannot be larger than 100k'); + } + try { + $this->bug->issuemanager->addPatch($this); + if (!@move_uploaded_file($file, $this->getPatchFullpath())) { + throw new ValidationException("Invalid attachment, could not move to attachment directory"); + } + foreach ($newobsoletes as $obsolete) { + $this->bug->issuemanager->obsoletePatch($this, $obsolete[0], $obsolete[1]); + } + $this->info['mimetype'] = $mime; + } catch (\Exception $e) { + @unlink($this->getPatchFullpath()); + $this->bug->issuemanager->removePatch($this); + throw $e; + } + } + + function __get($name) + { + if ($name == 'bug' || $name == 'comment') { + return $this->$name; + } + if ($name == 'file') { + return 'p' . $this->info['revision'] . '.patch.txt'; + } + } + + function __set($var, $value) + { + if ($var == 'name') { + if (!preg_match('/^[\w\-\.]+\z/', $value) || strlen($value) > 40) { + throw new ValidationException('Invalid name value, must be word characters,' . + 'dash, and must be less than 40 characters'); + } + } + } + function equals(Attachment $a) { - return $a == $this; + return ($a->patch == $this->info['patch']) && ($a->revision == $this->info['revision']); } } Added: pear2/BugManager/trunk/src/BugManager/BadMethodCallException.php =================================================================== --- pear2/BugManager/trunk/src/BugManager/BadMethodCallException.php (rev 0) +++ pear2/BugManager/trunk/src/BugManager/BadMethodCallException.php 2009-08-13 05:02:25 UTC (rev 287197) @@ -0,0 +1,27 @@ + + * @copyright 2009 Gregory Beaver + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id$ + * @link http://svn.php.net/repository/pear2/BugManager + */ + +/** + * Exception class for bad method calls + * + * @category Bugs + * @package PEAR2_BugManager + * @author Gregory Beaver + * @copyright 2009 Gregory Beaver + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://svn.php.net/repository/pear2/BugManager + */ +namespace pear2\BugManager; +class BadMethodCallException extends \BadMethodCallException implements Exception {} \ No newline at end of file Modified: pear2/BugManager/trunk/src/BugManager/IssueManager/Mysql.php =================================================================== --- pear2/BugManager/trunk/src/BugManager/IssueManager/Mysql.php 2009-08-13 03:32:47 UTC (rev 287196) +++ pear2/BugManager/trunk/src/BugManager/IssueManager/Mysql.php 2009-08-13 05:02:25 UTC (rev 287197) @@ -342,5 +342,67 @@ { $this->actorManager = $manager; } + + /** + * Stores a new attachment + */ + function addPatch(BugManager\Attachment $attachment) + { + $id = time(); + $test = $this->mysqli->query('SELECT revision FROM bugmanager_patchtracker + WHERE bug_id="' . $this->esc($attachment->bug->id) . '" + AND name="' . $this->esc($attachment->name) . '" + AND revision="' . $id . '"'); + $fail = 10; + while (--$fail && $test->num_rows) { + $id++; + $test->close(); + $test = $this->mysqli->query('SELECT revision FROM bugmanager_patchtracker + WHERE bug_id="' . $this->esc($attachment->bug->id) . '" + AND name="' . $this->esc($attachment->name) . '" + AND revision="' . $id . '"'); + } + $test->close(); + if (!$this->mysqli->query('INSERT INTO bugmanager_patchtracker + (bug_id, name, revision, handle) + VALUES("' . $this->esc($attachment->bug->id) . '", + "' . $this->esc($attachment->name) . '", + "' . $id . '", + "' . $this->esc($attachment->comment->commenter->uniqueId()) . '" + )"')) { + throw new UnexpectedValueException('Could not get unique attachment file name for issue #' . + $attachment->bug->id . ', attachment "'. $attachment->name . '"'); + } + $attachment->revision = $id; + } + + function removePatch(BugManager\Attachment $attachment) + { + if (!$attachment->revision) { + return; + } + $test = $this->mysqli->query('SELECT revision FROM bugmanager_patchtracker + WHERE bug_id="' . $this->esc($attachment->bug->id) . '" + AND name="' . $this->esc($attachment->name) . '" + AND revision="' . $this->esc($attachment->revision) . '"'); + if (!$test->num_rows) { + $test->close(); + return; + } + $this->mysqli->query('DELETE FROM bugmanager_patchtracker + WHERE bug_id="' . $this->esc($attachment->bug->id) . '" + AND name="' . $this->esc($attachment->name) . '" + AND revision="' . $this->esc($attachment->revision) . '"'); + } + + function obsoletePatch(BugManager\Attachment $attachment, $obsoletename, $obsoleterevision) + { + $this->mysqli->query('INSERT INTO bugmanager_obsoletes_patches + VALUES("' . $this->esc($attachment->bug->id) . '", + "' . $this->esc($attachment->name) . '", + "' . $this->esc($attachment->revision) . '", + "' . $this->esc($obsoletename) . '", + "' . $this->esc($obsoleterevision) . '")'); + } } Modified: pear2/BugManager/trunk/src/BugManager/IssueManagerInterface.php =================================================================== --- pear2/BugManager/trunk/src/BugManager/IssueManagerInterface.php 2009-08-13 03:32:47 UTC (rev 287196) +++ pear2/BugManager/trunk/src/BugManager/IssueManagerInterface.php 2009-08-13 05:02:25 UTC (rev 287197) @@ -70,4 +70,16 @@ * @param pear2\BugManager\ActorManagerInterface */ function setActorManager(ActorManagerInterface $manager); + /** + * retrieve a unique revision for a patch, and save it to the data store + */ + function addPatch(Attachment $attachment); + /** + * remove the patch from the data store + */ + function removePatch(Attachment $attachment); + /** + * Mark a previous patch/attachment as being obsoleted by the defined patch + */ + function obsoletePatch(Attachment $attachment, $obsoletename, $obsoleterevision); } Added: pear2/BugManager/trunk/src/BugManager/ValidationException.php =================================================================== --- pear2/BugManager/trunk/src/BugManager/ValidationException.php (rev 0) +++ pear2/BugManager/trunk/src/BugManager/ValidationException.php 2009-08-13 05:02:25 UTC (rev 287197) @@ -0,0 +1,27 @@ + + * @copyright 2009 Gregory Beaver + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id$ + * @link http://svn.php.net/repository/pear2/BugManager + */ + +/** + * Used for validation errors when setting bug values + * + * @category Bugs + * @package PEAR2_BugManager + * @author Gregory Beaver + * @copyright 2009 Gregory Beaver + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://svn.php.net/repository/pear2/BugManager + */ +namespace pear2\BugManager; +class ValidationException extends \DomainException implements Exception {} \ No newline at end of file Modified: pear2/BugManager/trunk/tests/Issue/setup.php.inc =================================================================== --- pear2/BugManager/trunk/tests/Issue/setup.php.inc 2009-08-13 03:32:47 UTC (rev 287196) +++ pear2/BugManager/trunk/tests/Issue/setup.php.inc 2009-08-13 05:02:25 UTC (rev 287197) @@ -1,7 +1,7 @@