[ Index ] |
PHP Cross Reference of Eventum |
[Summary view] [Print] [Text view]
1 <?php 2 /* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */ 3 // +----------------------------------------------------------------------+ 4 // | Eventum - Issue Tracking System | 5 // +----------------------------------------------------------------------+ 6 // | Copyright (c) 2003, 2004, 2005, 2006, 2007 MySQL AB | 7 // | | 8 // | This program is free software; you can redistribute it and/or modify | 9 // | it under the terms of the GNU General Public License as published by | 10 // | the Free Software Foundation; either version 2 of the License, or | 11 // | (at your option) any later version. | 12 // | | 13 // | This program is distributed in the hope that it will be useful, | 14 // | but WITHOUT ANY WARRANTY; without even the implied warranty of | 15 // | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 16 // | GNU General Public License for more details. | 17 // | | 18 // | You should have received a copy of the GNU General Public License | 19 // | along with this program; if not, write to: | 20 // | | 21 // | Free Software Foundation, Inc. | 22 // | 59 Temple Place - Suite 330 | 23 // | Boston, MA 02111-1307, USA. | 24 // +----------------------------------------------------------------------+ 25 // | Authors: João Prado Maia <jpm@mysql.com> | 26 // +----------------------------------------------------------------------+ 27 // 28 29 30 require_once (APP_INC_PATH . "class.error_handler.php"); 31 require_once (APP_INC_PATH . "class.auth.php"); 32 require_once (APP_INC_PATH . "class.user.php"); 33 require_once (APP_INC_PATH . "class.history.php"); 34 require_once (APP_INC_PATH . "class.misc.php"); 35 require_once (APP_INC_PATH . "class.date.php"); 36 require_once (APP_INC_PATH . "class.status.php"); 37 require_once (APP_INC_PATH . "class.issue.php"); 38 require_once (APP_INC_PATH . "class.workflow.php"); 39 40 /** 41 * Class designed to handle all business logic related to attachments being 42 * uploaded to issues in the application. 43 * 44 * @author João Prado Maia <jpm@mysql.com> 45 */ 46 class Attachment 47 { 48 /** 49 * Returns a list of file extensions that should be opened 50 * directly in the browser window as PHP source files. 51 * 52 * @access private 53 * @return array List of file extensions 54 */ 55 function _getPHPExtensions() 56 { 57 return array( 58 "php", 59 "php3", 60 "php4", 61 "phtml" 62 ); 63 } 64 65 66 /** 67 * Returns a list of file extensions that should be opened 68 * directly in the browser window and treated as text/plain 69 * files. 70 * 71 * @access private 72 * @return array List of file extensions 73 */ 74 function _getTextPlainExtensions() 75 { 76 return array( 77 'err', 78 'log', 79 'cnf', 80 'var', 81 'ini', 82 'java', 83 'txt' 84 ); 85 } 86 87 88 /** 89 * Returns a list of file extensions that should be opened 90 * directly in the browser window. 91 * 92 * @access private 93 * @return array List of file extensions 94 */ 95 function _getNoDownloadExtensions() 96 { 97 return array( 98 'jpg', 99 'jpeg', 100 'gif', 101 'png', 102 'bmp', 103 'html', 104 'htm', 105 'xml', 106 ); 107 } 108 109 110 /** 111 * Method used to output the headers and the binary data for 112 * an attachment file. 113 * 114 * @access public 115 * @param string $data The binary data of this file download 116 * @param string $filename The filename 117 * @param integer $filesize The size of this file 118 * @param string $filetype The mimetype of this file 119 * @return void 120 */ 121 function outputDownload(&$data, $filename, $filesize, $filetype) 122 { 123 $filename = Attachment::nameToSafe($filename); 124 $parts = pathinfo($filename); 125 if (in_array(strtolower(@$parts["extension"]), Attachment::_getPHPExtensions())) { 126 // instead of redirecting the user to a PHP script that may contain malicious code, we highlight the code 127 highlight_string($data); 128 } else { 129 if ((empty($filename)) && (!empty($filetype))) { 130 // inline images 131 header("Content-Type: $filetype"); 132 } elseif ((in_array(strtolower(@$parts["extension"]), Attachment::_getTextPlainExtensions())) && ($filesize < 5000)) { 133 // always force the browser to display the contents of these special files 134 header('Content-Type: text/plain'); 135 header("Content-Disposition: inline; filename=\"" . urlencode($filename) . "\""); 136 } else { 137 if (empty($filetype)) { 138 header("Content-Type: application/unknown"); 139 } else { 140 header("Content-Type: " . $filetype); 141 } 142 if (!in_array(strtolower(@$parts["extension"]), Attachment::_getNoDownloadExtensions())) { 143 header("Content-Disposition: attachment; filename=\"" . urlencode($filename) . "\""); 144 } else { 145 header("Content-Disposition: inline; filename=\"" . urlencode($filename) . "\""); 146 } 147 } 148 header("Content-Length: " . $filesize); 149 echo $data; 150 exit; 151 } 152 } 153 154 155 /** 156 * Method used to remove a specific file out of an existing attachment. 157 * 158 * @access public 159 * @param integer $iaf_id The attachment file ID 160 * @return -1 or -2 if the removal was not successful, 1 otherwise 161 */ 162 function removeIndividualFile($iaf_id) 163 { 164 $usr_id = Auth::getUserID(); 165 $iaf_id = Misc::escapeInteger($iaf_id); 166 $stmt = "SELECT 167 iat_iss_id 168 FROM 169 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment, 170 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment_file 171 WHERE 172 iaf_id=$iaf_id AND 173 iat_id=iaf_iat_id"; 174 if (Auth::getCurrentRole() < User::getRoleID("Manager")) { 175 $stmt .= " AND 176 iat_usr_id=$usr_id"; 177 } 178 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 179 if (PEAR::isError($res)) { 180 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 181 return -1; 182 } else { 183 if (empty($res)) { 184 return -2; 185 } else { 186 // check if the file is the only one in the attachment 187 $stmt = "SELECT 188 iat_id 189 FROM 190 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment, 191 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment_file 192 WHERE 193 iaf_id=$iaf_id AND 194 iaf_iat_id=iat_id"; 195 $attachment_id = $GLOBALS["db_api"]->dbh->getOne($stmt); 196 197 $res = Attachment::getFileList($attachment_id); 198 if (@count($res) > 1) { 199 Attachment::removeFile($iaf_id); 200 } else { 201 Attachment::remove($attachment_id); 202 } 203 return 1; 204 } 205 } 206 } 207 208 209 /** 210 * Method used to return the details for a given attachment. 211 * 212 * @access public 213 * @param integer $file_id The attachment ID 214 * @return array The details of the attachment 215 */ 216 function getDetails($file_id) 217 { 218 $file_id = Misc::escapeInteger($file_id); 219 $stmt = "SELECT 220 * 221 FROM 222 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment, 223 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment_file 224 WHERE 225 iat_id=iaf_iat_id AND 226 iaf_id=$file_id"; 227 $res = $GLOBALS["db_api"]->dbh->getRow($stmt, DB_FETCHMODE_ASSOC); 228 if (PEAR::isError($res)) { 229 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 230 return ""; 231 } else { 232 // don't allow customers to reach internal only files 233 if (($res['iat_status'] == 'internal') 234 && (User::getRoleByUser(Auth::getUserID(), Issue::getProjectID($res['iat_iss_id'])) <= User::getRoleID('Customer'))) { 235 return ''; 236 } else { 237 return $res; 238 } 239 } 240 } 241 242 243 /** 244 * Removes all attachments (and associated files) related to a set 245 * of specific issues. 246 * 247 * @access public 248 * @param array $ids The issue IDs that need to be removed 249 * @return boolean Whether the removal worked or not 250 */ 251 function removeByIssues($ids) 252 { 253 $items = @implode(", ", $ids); 254 $stmt = "SELECT 255 iat_id 256 FROM 257 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment 258 WHERE 259 iat_iss_id IN ($items)"; 260 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 261 if (PEAR::isError($res)) { 262 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 263 return false; 264 } else { 265 for ($i = 0; $i < count($res); $i++) { 266 Attachment::remove($res[$i]); 267 } 268 return true; 269 } 270 } 271 272 273 /** 274 * Method used to remove attachments from the database. 275 * 276 * @param integer $iat_id attachment_id. 277 * @param boolean $add_history whether to add history entry. 278 * @access public 279 * @return integer Numeric code used to check for any errors 280 */ 281 function remove($iat_id, $add_history = true) 282 { 283 $iat_id = Misc::escapeInteger($iat_id); 284 $usr_id = Auth::getUserID(); 285 $stmt = "SELECT 286 iat_iss_id 287 FROM 288 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment 289 WHERE 290 iat_id=$iat_id"; 291 if (Auth::getCurrentRole() < User::getRoleID("Manager")) { 292 $stmt .= " AND 293 iat_usr_id=$usr_id"; 294 } 295 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 296 if (PEAR::isError($res)) { 297 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 298 return -1; 299 } else { 300 if (empty($res)) { 301 return -2; 302 } else { 303 $issue_id = $res; 304 $files = Attachment::getFileList($iat_id); 305 $stmt = "DELETE FROM 306 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment 307 WHERE 308 iat_id=$iat_id AND 309 iat_iss_id=$issue_id"; 310 $res = $GLOBALS["db_api"]->dbh->query($stmt); 311 if (PEAR::isError($res)) { 312 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 313 return -1; 314 } 315 for ($i = 0; $i < count($files); $i++) { 316 Attachment::removeFile($files[$i]['iaf_id']); 317 } 318 if ($add_history) { 319 Issue::markAsUpdated($usr_id); 320 // need to save a history entry for this 321 History::add($issue_id, $usr_id, History::getTypeID('attachment_removed'), 'Attachment removed by ' . User::getFullName($usr_id)); 322 } 323 return 1; 324 } 325 } 326 } 327 328 /** 329 * Method used to remove a specific file from an attachment, since every 330 * attachment can have several files associated with it. 331 * 332 * @access public 333 * @param integer $iaf_id The attachment file ID 334 * @return void 335 */ 336 function removeFile($iaf_id) 337 { 338 $iaf_id = Misc::escapeInteger($iaf_id); 339 $stmt = "DELETE FROM 340 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment_file 341 WHERE 342 iaf_id=" . $iaf_id; 343 $res = $GLOBALS["db_api"]->dbh->query($stmt); 344 if (PEAR::isError($res)) { 345 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 346 return -1; 347 } 348 } 349 350 351 /** 352 * Method used to get the full listing of files for a specific attachment. 353 * 354 * @access public 355 * @param integer $attachment_id The attachment ID 356 * @return array The full list of files 357 */ 358 function getFileList($attachment_id) 359 { 360 $attachment_id = Misc::escapeInteger($attachment_id); 361 $stmt = "SELECT 362 iaf_id, 363 iaf_filename, 364 iaf_filesize 365 FROM 366 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment_file 367 WHERE 368 iaf_iat_id=$attachment_id"; 369 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 370 if (PEAR::isError($res)) { 371 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 372 return ""; 373 } else { 374 for ($i = 0; $i < count($res); $i++) { 375 $res[$i]["iaf_filesize"] = Misc::formatFileSize($res[$i]["iaf_filesize"]); 376 } 377 return $res; 378 } 379 } 380 381 382 /** 383 * Method used to return the full list of attachments related to a specific 384 * issue in the database. 385 * 386 * @access public 387 * @param integer $issue_id The issue ID 388 * @return array The full list of attachments 389 */ 390 function getList($issue_id) 391 { 392 $issue_id = Misc::escapeInteger($issue_id); 393 $usr_id = Auth::getUserID(); 394 $prj_id = Issue::getProjectID($issue_id); 395 396 $stmt = "SELECT 397 iat_id, 398 iat_usr_id, 399 usr_full_name, 400 iat_created_date, 401 iat_description, 402 iat_unknown_user, 403 iat_status 404 FROM 405 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment, 406 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user 407 WHERE 408 iat_iss_id=$issue_id AND 409 iat_usr_id=usr_id"; 410 if (User::getRoleByUser($usr_id, $prj_id) <= User::getRoleID('Customer')) { 411 $stmt .= " AND iat_status='public' "; 412 } 413 $stmt .= " 414 ORDER BY 415 iat_created_date ASC"; 416 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 417 if (PEAR::isError($res)) { 418 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 419 return ""; 420 } else { 421 for ($i = 0; $i < count($res); $i++) { 422 $res[$i]["iat_description"] = Link_Filter::processText(Issue::getProjectID($issue_id), nl2br(htmlspecialchars($res[$i]["iat_description"]))); 423 $res[$i]["files"] = Attachment::getFileList($res[$i]["iat_id"]); 424 $res[$i]["iat_created_date"] = Date_API::getFormattedDate($res[$i]["iat_created_date"]); 425 426 // if there is an unknown user, user that instead of the user_full_name 427 if (!empty($res[$i]["iat_unknown_user"])) { 428 $res[$i]["usr_full_name"] = $res[$i]["iat_unknown_user"]; 429 } 430 } 431 return $res; 432 } 433 } 434 435 436 /** 437 * Method used to associate an attachment to an issue, and all of its 438 * related files. It also notifies any subscribers of this new attachment. 439 * 440 * Error codes: 441 * -1 - An error occurred while trying to process the uploaded file. 442 * -2 - The uploaded file is already attached to the current issue. 443 * 1 - The uploaded file was associated with the issue. 444 * 445 * @access public 446 * @param integer $usr_id The user ID 447 * @param string $status The attachment status 448 * @return integer Numeric code used to check for any errors 449 */ 450 function attach($usr_id, $status = 'public') 451 { 452 $files = array(); 453 for ($i = 0; $i < count($_FILES["attachment"]["name"]); $i++) { 454 $filename = @$_FILES["attachment"]["name"][$i]; 455 if (empty($filename)) { 456 continue; 457 } 458 $blob = Misc::getFileContents($_FILES["attachment"]["tmp_name"][$i]); 459 if (empty($blob)) { 460 return -1; 461 } 462 $files[] = array( 463 "filename" => $filename, 464 "type" => $_FILES['attachment']['type'][$i], 465 "blob" => $blob 466 ); 467 } 468 if (count($files) < 1) { 469 return -1; 470 } 471 if ($status == 'internal') { 472 $internal_only = true; 473 } else { 474 $internal_only = false; 475 } 476 $attachment_id = Attachment::add($_POST["issue_id"], $usr_id, @$_POST["file_description"], $internal_only); 477 foreach ($files as $file) { 478 $res = Attachment::addFile($attachment_id, $file["filename"], $file["type"], $file["blob"]); 479 if ($res !== true) { 480 // we must rollback whole attachment (all files) 481 Attachment::remove($attachment_id, false); 482 return -1; 483 } 484 } 485 486 Issue::markAsUpdated($_POST["issue_id"], "file uploaded"); 487 // need to save a history entry for this 488 History::add($_POST["issue_id"], $usr_id, History::getTypeID('attachment_added'), 'Attachment uploaded by ' . User::getFullName($usr_id)); 489 490 // if there is customer integration, mark last customer action 491 if ((Customer::hasCustomerIntegration(Issue::getProjectID($_POST["issue_id"]))) && (User::getRoleByUser($usr_id, Issue::getProjectID($_POST["issue_id"])) == User::getRoleID('Customer'))) { 492 Issue::recordLastCustomerAction($_POST["issue_id"]); 493 } 494 495 Workflow::handleAttachment(Issue::getProjectID($_POST["issue_id"]), $_POST["issue_id"], $usr_id); 496 497 // send notifications for the issue being updated 498 // XXX: eventually need to restrict the list of people who receive a notification about this in a better fashion 499 if ($status == 'public') { 500 Notification::notify($_POST["issue_id"], 'files', $attachment_id); 501 } 502 return 1; 503 } 504 505 506 /** 507 * Method used to add files to a specific attachment in the database. 508 * 509 * @access public 510 * @param integer $attachment_id The attachment ID 511 * @param string $filename The filename to be added 512 * @return boolean 513 */ 514 function addFile($attachment_id, $filename, $filetype, &$blob) 515 { 516 $filesize = strlen($blob); 517 $stmt = "INSERT INTO 518 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment_file 519 ( 520 iaf_iat_id, 521 iaf_filename, 522 iaf_filesize, 523 iaf_filetype, 524 iaf_file 525 ) VALUES ( 526 $attachment_id, 527 '" . Misc::escapeString($filename) . "', 528 '" . $filesize . "', 529 '" . Misc::escapeString($filetype) . "', 530 '" . Misc::escapeString($blob) . "' 531 )"; 532 $res = $GLOBALS["db_api"]->dbh->query($stmt); 533 if (PEAR::isError($res)) { 534 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 535 return false; 536 } else { 537 return true; 538 } 539 } 540 541 542 /** 543 * Method used to add an attachment to the database. 544 * 545 * @access public 546 * @param integer $issue_id The issue ID 547 * @param integer $usr_id The user ID 548 * @param string $description The description for this new attachment 549 * @param boolean $internal_only Whether this attachment is supposed to be internal only or not 550 * @param string $unknown_user The email of the user who originally sent this email, who doesn't have an account. 551 * @param integer $associated_note_id The note ID that these attachments should be associated with 552 * @return integer The new attachment ID 553 */ 554 function add($issue_id, $usr_id, $description, $internal_only = FALSE, $unknown_user = FALSE, $associated_note_id = FALSE) 555 { 556 if ($internal_only) { 557 $attachment_status = 'internal'; 558 } else { 559 $attachment_status = 'public'; 560 } 561 562 $stmt = "INSERT INTO 563 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_attachment 564 ( 565 iat_iss_id, 566 iat_usr_id, 567 iat_created_date, 568 iat_description, 569 iat_status"; 570 if ($unknown_user != false) { 571 $stmt .= ", iat_unknown_user "; 572 } 573 if ($associated_note_id != false) { 574 $stmt .= ", iat_not_id "; 575 } 576 $stmt .=") VALUES ( 577 $issue_id, 578 $usr_id, 579 '" . Date_API::getCurrentDateGMT() . "', 580 '" . Misc::escapeString($description) . "', 581 '" . Misc::escapeString($attachment_status) . "'"; 582 if ($unknown_user != false) { 583 $stmt .= ", '" . Misc::escapeString($unknown_user) . "'"; 584 } 585 if ($associated_note_id != false) { 586 $stmt .= ", " . Misc::escapeInteger($associated_note_id); 587 } 588 $stmt .= " )"; 589 $res = $GLOBALS["db_api"]->dbh->query($stmt); 590 if (PEAR::isError($res)) { 591 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 592 return false; 593 } else { 594 return $GLOBALS["db_api"]->get_last_insert_id(); 595 } 596 } 597 598 599 /** 600 * Method used to replace unsafe characters by safe characters. 601 * 602 * Side-effects: if $name is not in ISO8859-1 encoding, not very logical 603 * replacements are done. Eventually the non-ASCII characters are stripped. 604 * 605 * @access public 606 * @param string $name The name of the file to be checked. In ISO8859-1 encoding. 607 * @param integer $maxlen The maximum length of the filename 608 * @return string The 'safe' version of the filename. Always in US-ASCII encoding. 609 */ 610 function nameToSafe($name, $maxlen = 250) 611 { 612 // using hex bytes as these need to be *bytes*, not dependant on sourcefile encoding. 613 $noalpha = "\xe1\xe9\xed\xf3\xfa\xe0\xe8\xec\xf2\xf9\xe4\xeb\xef\xf6\xfc\xc1\xc9\xcd\xd3\xda\xc0\xc8\xcc\xd2\xd9\xc4\xcb\xcf\xd6\xdc\xe2\xea\xee\xf4\xfb\xc2\xca\xce\xd4\xdb\xf1\xe7\xc7\x40"; 614 $alpha = 'aeiouaeiouaeiouAEIOUAEIOUAEIOUaeiouAEIOUncCa'; 615 $name = substr($name, 0, $maxlen); 616 $name = strtr($name, $noalpha, $alpha); 617 // not permitted chars are replaced with "_" 618 return ereg_replace('[^a-zA-Z0-9,._\+\()\-]', '_', $name); 619 } 620 621 622 /** 623 * Returns the current maximum file upload size. 624 * 625 * @access public 626 * @return string A string containing the formatted max file size. 627 */ 628 function getMaxAttachmentSize() 629 { 630 $size = ini_get('upload_max_filesize'); 631 // check if this directive uses the string version (e.g. 256M) 632 if (strstr($size, 'M')) { 633 $size = str_replace('M', '', $size); 634 return Misc::formatFileSize($size * 1024 * 1024); 635 } else { 636 return Misc::formatFileSize($size); 637 } 638 } 639 } 640 641 // benchmarking the included file (aka setup time) 642 if (APP_BENCHMARK) { 643 $GLOBALS['bench']->setMarker('Included Attachment Class'); 644 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Dec 19 21:21:33 2007 | Cross-referenced by PHPXref 0.7 |