diff --git a/src/IDF/Form/IssueCreate.php b/src/IDF/Form/IssueCreate.php index 497d910..0743e72 100644 --- a/src/IDF/Form/IssueCreate.php +++ b/src/IDF/Form/IssueCreate.php @@ -64,6 +64,26 @@ class IDF_Form_IssueCreate extends Pluf_Form 'rows' => 13, ), )); + $upload_path = Pluf::f('upload_issue_path', false); + if (false === $upload_path) { + throw new Pluf_Exception_SettingError(__('The "upload_issue_path" configuration variable was not set.')); + } + $md5 = md5(rand().microtime().Pluf_Utils::getRandomString()); + // We add .dummy to try to mitigate security issues in the + // case of someone allowing the upload path to be accessible + // to everybody. + $filename = substr($md5, 0, 2).'/'.substr($md5, 2, 2).'/'.substr($md5, 4).'/%s.dummy'; + $this->fields['attachment'] = new Pluf_Form_Field_File( + array('required' => false, + 'label' => __('Attach a file'), + 'move_function_params' => + array('upload_path' => $upload_path, + 'upload_path_create' => true, + 'file_name' => $filename, + ) + ) + ); + if ($this->show_full) { $this->fields['status'] = new Pluf_Form_Field_Varchar( array('required' => true, @@ -226,6 +246,15 @@ class IDF_Form_IssueCreate extends Pluf_Form $comment->content = $this->cleaned_data['content']; $comment->submitter = $this->user; $comment->create(); + // If we have a file, create the IDF_IssueFile and attach + // it to the comment. + if ($this->cleaned_data['attachment']) { + $file = new IDF_IssueFile(); + $file->attachment = $this->cleaned_data['attachment']; + $file->submitter = $this->user; + $file->comment = $comment; + $file->create(); + } return $issue; } throw new Exception(__('Cannot save the model from an invalid form.')); diff --git a/src/IDF/Form/IssueUpdate.php b/src/IDF/Form/IssueUpdate.php index 69380bf..550889e 100644 --- a/src/IDF/Form/IssueUpdate.php +++ b/src/IDF/Form/IssueUpdate.php @@ -60,6 +60,26 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate 'rows' => 9, ), )); + $upload_path = Pluf::f('upload_issue_path', false); + if (false === $upload_path) { + throw new Pluf_Exception_SettingError(__('The "upload_issue_path" configuration variable was not set.')); + } + $md5 = md5(rand().microtime().Pluf_Utils::getRandomString()); + // We add .dummy to try to mitigate security issues in the + // case of someone allowing the upload path to be accessible + // to everybody. + $filename = substr($md5, 0, 2).'/'.substr($md5, 2, 2).'/'.substr($md5, 4).'/%s.dummy'; + $this->fields['attachment'] = new Pluf_Form_Field_File( + array('required' => false, + 'label' => __('Attach a file'), + 'move_function_params' => + array('upload_path' => $upload_path, + 'upload_path_create' => true, + 'file_name' => $filename, + ) + ) + ); + if ($this->show_full) { $this->fields['status'] = new Pluf_Form_Field_Varchar( array('required' => true, @@ -257,6 +277,13 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate $this->issue->submitter != $this->user->id) { $this->issue->setAssoc($this->user); // interested user. } + if ($this->cleaned_data['attachment']) { + $file = new IDF_IssueFile(); + $file->attachment = $this->cleaned_data['attachment']; + $file->submitter = $this->user; + $file->comment = $comment; + $file->create(); + } return $this->issue; } throw new Exception(__('Cannot save the model from an invalid form.')); diff --git a/src/IDF/IssueFile.php b/src/IDF/IssueFile.php new file mode 100644 index 0000000..f4367dd --- /dev/null +++ b/src/IDF/IssueFile.php @@ -0,0 +1,125 @@ +_a['table'] = 'idf_issuefiles'; + $this->_a['model'] = __CLASS__; + $this->_a['cols'] = array( + // It is mandatory to have an "id" column. + 'id' => + array( + 'type' => 'Pluf_DB_Field_Sequence', + //It is automatically added. + 'blank' => true, + ), + 'comment' => + array( + 'type' => 'Pluf_DB_Field_Foreignkey', + 'model' => 'IDF_IssueComment', + 'blank' => false, + 'verbose' => __('comment'), + 'relate_name' => 'attachment', + ), + 'submitter' => + array( + 'type' => 'Pluf_DB_Field_Foreignkey', + 'model' => 'Pluf_User', + 'blank' => false, + 'verbose' => __('submitter'), + ), + 'filename' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => true, + 'size' => 100, + 'verbose' => __('file name'), + ), + 'attachment' => + array( + 'type' => 'Pluf_DB_Field_File', + 'blank' => false, + 'verbose' => __('the file'), + ), + 'filesize' => + array( + 'type' => 'Pluf_DB_Field_Integer', + 'blank' => true, + 'verbose' => __('file size'), + 'help_text' => 'Size in bytes.', + ), + 'type' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'size' => 10, + 'verbose' => __('type'), + 'choices' => array( + __('Image') => 'img', + __('Other') => 'other', + ), + 'default' => 'other', + 'help_text' => 'The type is to display a thumbnail of the image.', + ), + 'creation_dtime' => + array( + 'type' => 'Pluf_DB_Field_Datetime', + 'blank' => true, + 'verbose' => __('creation date'), + ), + 'modif_dtime' => + array( + 'type' => 'Pluf_DB_Field_Datetime', + 'blank' => true, + 'verbose' => __('modification date'), + ), + ); + } + + function preSave($create=false) + { + if ($this->id == '') { + $this->creation_dtime = gmdate('Y-m-d H:i:s'); + $file = Pluf::f('upload_issue_path').'/'.$this->attachment; + $this->filesize = filesize($file); + // remove .dummy + $this->filename = substr(basename($file), 0, -6); + $img_extensions = array('jpeg', 'jpg', 'png', 'gif'); + $info = pathinfo($this->filename); + if (in_array(strtolower($info['extension']), $img_extensions)) { + $this->type = 'img'; + } else { + $this->type = 'other'; + } + } + $this->modif_dtime = gmdate('Y-m-d H:i:s'); + } +} diff --git a/src/IDF/Migrations/3Attachments.php b/src/IDF/Migrations/3Attachments.php new file mode 100644 index 0000000..1838a05 --- /dev/null +++ b/src/IDF/Migrations/3Attachments.php @@ -0,0 +1,52 @@ +model = new $model(); + $schema->createTables(); + } +} + +function IDF_Migrations_3Attachments_down($params=null) +{ + $models = array( + 'IDF_IssueFile', + ); + $db = Pluf::db(); + $schema = new Pluf_DB_Schema($db); + foreach ($models as $model) { + $schema->model = new $model(); + $schema->dropTables(); + } +} \ No newline at end of file diff --git a/src/IDF/Migrations/Install.php b/src/IDF/Migrations/Install.php index 61d9ca1..8d6a9ad 100644 --- a/src/IDF/Migrations/Install.php +++ b/src/IDF/Migrations/Install.php @@ -37,6 +37,7 @@ function IDF_Migrations_Install_setup($params=null) 'IDF_Conf', 'IDF_Upload', 'IDF_Search_Occ', + 'IDF_IssueFile', ); $db = Pluf::db(); $schema = new Pluf_DB_Schema($db); diff --git a/src/IDF/Views/Issue.php b/src/IDF/Views/Issue.php index c6c3f9e..a42fd7c 100644 --- a/src/IDF/Views/Issue.php +++ b/src/IDF/Views/Issue.php @@ -141,7 +141,9 @@ class IDF_Views_Issue 'project' => $prj, 'user' => $request->user); if ($request->method == 'POST') { - $form = new IDF_Form_IssueCreate($request->POST, $params); + $form = new IDF_Form_IssueCreate(array_merge($request->POST, + $request->FILES), + $params); if ($form->isValid()) { $issue = $form->save(); $url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::index', @@ -246,7 +248,9 @@ class IDF_Views_Issue 'issue' => $issue, ); if ($request->method == 'POST') { - $form = new IDF_Form_IssueUpdate($request->POST, $params); + $form = new IDF_Form_IssueUpdate(array_merge($request->POST, + $request->FILES), + $params); if ($form->isValid()) { $issue = $form->save(); $url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::index', @@ -307,6 +311,22 @@ class IDF_Views_Issue $request); } + + /** + * Download a given attachment. + */ + public $getAttachment_precond = array('IDF_Precondition::accessIssues'); + public function getAttachment($request, $match) + { + $prj = $request->project; + $attach = Pluf_Shortcuts_GetObjectOr404('IDF_IssueFile', $match[2]); + $prj->inOr404($attach->get_comment()->get_issue()); + $res = new Pluf_HTTP_Response_File(Pluf::f('upload_issue_path').'/'.$attach->attachment, + 'application/octet-stream'); + $res->headers['Content-Disposition'] = 'attachment; filename="'.$attach->filename.'"'; + return $res; + } + /** * View list of issues for a given project with a given status. */ diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index 149b97d..6e0c295 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -87,6 +87,13 @@ $cfg['url_media'] = 'http://projects.ceondo.com/media'; $cfg['url_upload'] = 'http://projects/ceondo.com/media/upload'; $cfg['upload_path'] = '/path/to/media/upload'; +# +# The following path *MUST NOT* be accessible through a web browser +# as user will be able to upload .html, .php files and this can +# create *TERRIBLE* security issues. +# +$cfg['upload_issue_path'] = '/path/to/attachments'; + $cfg['login_success_url'] = $cfg['url_base'].$cfg['idf_base']; $cfg['after_logout_page'] = $cfg['url_base'].$cfg['idf_base']; diff --git a/src/IDF/conf/views.php b/src/IDF/conf/views.php index c14c6a2..4b89173 100644 --- a/src/IDF/conf/views.php +++ b/src/IDF/conf/views.php @@ -133,6 +133,12 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/my/(\w+)/$#', 'model' => 'IDF_Views_Issue', 'method' => 'myIssues'); +$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/attachment/(\d+)/(.*)$#', + 'base' => $base, + 'priority' => 4, + 'model' => 'IDF_Views_Issue', + 'method' => 'getAttachment'); + // ---------- SCM ---------------------------------------- $ctl[] = array('regex' => '#^/p/([\-\w]+)/source/tree/(\w+)/$#', diff --git a/src/IDF/relations.php b/src/IDF/relations.php index 6b81b94..3e12dfe 100644 --- a/src/IDF/relations.php +++ b/src/IDF/relations.php @@ -26,6 +26,7 @@ $m['IDF_Tag'] = array('relate_to' => array('IDF_Project')); $m['IDF_Issue'] = array('relate_to' => array('IDF_Project', 'Pluf_User', 'IDF_Tag'), 'relate_to_many' => array('IDF_Tag', 'Pluf_User')); $m['IDF_IssueComment'] = array('relate_to' => array('IDF_Issue', 'Pluf_User')); +$m['IDF_IssueFile'] = array('relate_to' => array('IDF_IssueComment', 'Pluf_User')); $m['IDF_Upload'] = array('relate_to' => array('IDF_Project', 'Pluf_User'), 'relate_to_many' => array('IDF_Tag')); $m['IDF_Search_Occ'] = array('relate_to' => array('IDF_Project'),); diff --git a/src/IDF/templates/issues/create.html b/src/IDF/templates/issues/create.html index 5357a89..7f8bde7 100644 --- a/src/IDF/templates/issues/create.html +++ b/src/IDF/templates/issues/create.html @@ -23,6 +23,12 @@ {if $form.f.content.errors}{$form.f.content.fieldErrors}{/if} {$form.f.content|unsafe} + + +{$form.f.attachment.labelTag}: +{if $form.f.attachment.errors}{$form.f.attachment.fieldErrors}{/if} +{$form.f.attachment|unsafe} + {if $isOwner or $isMember} {$form.f.status.labelTag}: diff --git a/src/IDF/templates/issues/view.html b/src/IDF/templates/issues/view.html index f29def8..f4e9e56 100644 --- a/src/IDF/templates/issues/view.html +++ b/src/IDF/templates/issues/view.html @@ -15,7 +15,12 @@ {/if}
{if strlen($c.content) > 0}{issuetext $c.content, $request}{else}{trans '(No comments were given for this change.)'}{/if}
- +{assign $attachments = $c.get_attachment_list()} +{if $attachments.count() > 0} +
+
{/if} {if $i> 0 and $c.changedIssue()}
{foreach $c.changes as $w => $v} @@ -54,7 +59,12 @@ {$form.f.content|unsafe} -{if $isOwner or $isMember} + +{$form.f.attachment.labelTag}: +{if $form.f.attachment.errors}{$form.f.attachment.fieldErrors}{/if} +{$form.f.attachment|unsafe} + +{if $isOwner or $isMember} {$form.f.summary.labelTag}: {if $form.f.summary.errors}{$form.f.summary.fieldErrors}{/if} diff --git a/www/media/idf/css/style.css b/www/media/idf/css/style.css index 656daaf..ba1c55a 100644 --- a/www/media/idf/css/style.css +++ b/www/media/idf/css/style.css @@ -228,6 +228,11 @@ span.nobrk { hr { visibility: hidden; } +div.attach { + border-top: 2px solid #d3d7cf; + width: 40%; +} + textarea { font-family: monospace; }