5 * Copyright 1999-2001, VA Linux Systems, Inc.
6 * Copyright 2002-2004, GForge, LLC
7 * Copyright 2009, Roland Mas
8 * Copyright 2009, Alcatel-Lucent
10 * This file is part of FusionForge.
12 * FusionForge is free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published
14 * by the Free Software Foundation; either version 2 of the License,
15 * or (at your option) any later version.
17 * FusionForge is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
22 * You should have received a copy of the GNU General Public License
23 * along with FusionForge; if not, write to the Free Software
24 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
29 * Standard Alcatel-Lucent disclaimer for contributing to open source
31 * "The Artifact ("Contribution") has not been tested and/or
32 * validated for release as or in products, combinations with products or
33 * other commercial use. Any use of the Contribution is entirely made at
34 * the user's own responsibility and the user can not rely on any features,
35 * functionalities or performances Alcatel-Lucent has attributed to the
38 * THE CONTRIBUTION BY ALCATEL-LUCENT IS PROVIDED AS IS, WITHOUT WARRANTY
39 * OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
40 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, COMPLIANCE,
41 * NON-INTERFERENCE AND/OR INTERWORKING WITH THE SOFTWARE TO WHICH THE
42 * CONTRIBUTION HAS BEEN MADE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL
43 * ALCATEL-LUCENT BE LIABLE FOR ANY DAMAGES OR OTHER LIABLITY, WHETHER IN
44 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
45 * CONTRIBUTION OR THE USE OR OTHER DEALINGS IN THE CONTRIBUTION, WHETHER
46 * TOGETHER WITH THE SOFTWARE TO WHICH THE CONTRIBUTION RELATES OR ON A STAND
50 require_once $gfcommon.'include/Error.class.php';
51 require_once $gfcommon.'tracker/ArtifactMessage.class.php';
52 require_once $gfcommon.'tracker/ArtifactExtraField.class.php';
53 require_once $gfcommon.'tracker/ArtifactWorkflow.class.php';
55 // This string is used when sending the notification mail for identifying the
57 define('ARTIFACT_MAIL_MARKER', '#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+#+');
60 * Factory method which creates an Artifact from an artifact ID
62 * @param int The artifact ID
63 * @param array The result array, if it's passed in
64 * @return object Artifact object
66 function &artifact_get_object($artifact_id,$data=false) {
68 if (!isset($ARTIFACT_OBJ["_".$artifact_id."_"])) {
70 //the db result handle was passed in
72 $res = db_query_params ('SELECT * FROM artifact_vw WHERE artifact_id=$1',
73 array ($artifact_id)) ;
74 if (db_numrows($res) <1 ) {
75 $ARTIFACT_OBJ["_".$artifact_id."_"]=false;
78 $data =& db_fetch_array($res);
80 $ArtifactType =& artifactType_get_object($data["group_artifact_id"]);
81 $ARTIFACT_OBJ["_".$artifact_id."_"]= new Artifact($ArtifactType,$data);
83 return $ARTIFACT_OBJ["_".$artifact_id."_"];
86 class Artifact extends Error {
91 * @var int $status_res.
96 * Artifact Type object.
98 * @var object $ArtifactType.
103 * Array of artifact data.
105 * @var array $data_array.
110 * Array of artifact data for extra fields defined by Admin.
112 * @var array $extra_field_data.
114 var $extra_field_data;
117 * Array of ArtifactFile objects.
124 * Database result set of related tasks
126 * @var result $relatedtasks
131 * Artifact - constructor.
133 * @param object The ArtifactType object.
134 * @param integer (primary key from database OR complete assoc array)
135 * ONLY OPTIONAL WHEN YOU PLAN TO IMMEDIATELY CALL ->create()
136 * @return boolean success.
138 function Artifact(&$ArtifactType, $data=false) {
141 $this->ArtifactType =& $ArtifactType;
143 //was ArtifactType legit?
144 if (!$ArtifactType || !is_object($ArtifactType)) {
145 $this->setError('Artifact: No Valid ArtifactType');
149 //did ArtifactType have an error?
150 if ($ArtifactType->isError()) {
151 $this->setError('Artifact: '.$ArtifactType->getErrorMessage());
156 // make sure this person has permission to view artifacts
158 if (!$this->ArtifactType->userCanView()) {
159 $this->setError(_('Artifact: Only group members can view private artifact types'));
164 // set up data structures
167 if (is_array($data)) {
168 $this->data_array =& $data;
170 // Should verify ArtifactType ID
174 if (!$this->fetchData($data)) {
185 * create - construct a new Artifact in the database.
187 * @param string The artifact summary.
188 * @param string Details of the artifact.
189 * @param int The ID of the user to which this artifact is to be assigned.
190 * @param int The artifacts priority.
191 * @param array Array of extra fields like: array(15=>'foobar',22=>'1');
192 * @return id on success / false on failure.
194 function create( $summary, $details, $assigned_to=100, $priority=3, $extra_fields=array()) {
196 // make sure this person has permission to add artifacts
198 if (!$this->ArtifactType->isPublic()) {
200 // Only admins can post/modify private artifacts
204 // ape: Disabled, private means only restricted to members. So, no special rules #2503.
205 // if (!$this->ArtifactType->userIsAdmin()) {
206 // $this->setError(_('Artifact: Only Artifact Admins Can Modify Private ArtifactTypes'));
214 if (session_loggedin()) {
217 if ($this->ArtifactType->allowsAnon()) {
220 $this->setError(_('Artifact: This ArtifactType Does Not Allow Anonymous Submissions. Please Login.'));
229 $this->setError(_('Artifact: Message Summary Is Required'));
233 $this->setError(_('Artifact: Message Body Is Required'));
242 // if (!$status_id) {
243 $status_id=1; // on creation, status is set to "open"
246 // They may be using an extra field "status" box so we have to remap
247 // the status_id based on the extra field - this keeps the counters
250 $status_id=$this->ArtifactType->remapStatus($status_id,$extra_fields);
252 $this->setError(_('Artifact: Error remapping status'));
258 $res = db_query_params ('INSERT INTO artifact
259 (group_artifact_id,status_id,priority,
260 submitted_by,assigned_to,open_date,summary,details)
261 VALUES ($1,$2,$3,$4,$5,$6,$7,$8)',
262 array ($this->ArtifactType->getID(),
268 htmlspecialchars($summary),
269 htmlspecialchars($details))) ;
271 $this->setError('Artifact: '.db_error());
276 $artifact_id=db_insertid($res,'artifact','artifact_id');
278 if (!$res || !$artifact_id) {
279 $this->setError('Artifact: '.db_error());
284 // Now set up our internal data structures
286 if (!$this->fetchData($artifact_id)) {
290 // the changes to the extra fields will be logged in this array.
291 // (we won't use it however)
292 $extra_field_changes = array();
293 if (!$this->updateExtraFields($extra_fields,$extra_field_changes)) {
299 // now send an email if appropriate
301 $this->mailFollowup(1);
309 * fetchData - re-fetch the data for this Artifact from the database.
311 * @param int The artifact ID.
312 * @return boolean success.
314 function fetchData($artifact_id) {
315 $res = db_query_params ('SELECT * FROM artifact_vw WHERE artifact_id=$1 AND group_artifact_id=$2',
317 $this->ArtifactType->getID())) ;
318 if (!$res || db_numrows($res) < 1) {
319 $this->setError('Artifact: Invalid ArtifactID');
322 $this->data_array =& db_fetch_array($res);
323 db_free_result($res);
328 * getArtifactType - get the ArtifactType Object this Artifact is associated with.
330 * @return object ArtifactType.
332 function &getArtifactType() {
333 return $this->ArtifactType;
337 * getID - get this ArtifactID.
339 * @return int The artifact_id #.
342 return $this->data_array['artifact_id'];
346 * getStatusID - get open/closed/deleted flag.
348 * @return int Status: (1) Open, (2) Closed, (3) Deleted.
350 function getStatusID() {
351 return $this->data_array['status_id'];
355 * getStatusName - get open/closed/deleted text.
357 * @return string The status name.
359 function getStatusName() {
360 return $this->data_array['status_name'];
364 * getPriority - get priority flag.
366 * @return int priority.
368 function getPriority() {
369 return $this->data_array['priority'];
373 * getSubmittedBy - get ID of submitter.
375 * @return int user_id of submitter.
377 function getSubmittedBy() {
378 return $this->data_array['submitted_by'];
382 * getSubmittedEmail - get email of submitter.
384 * @return string The email of submitter.
386 function getSubmittedEmail() {
387 return $this->data_array['submitted_email'];
391 * getSubmittedRealName - get real name of submitter.
393 * @return string The real name of submitter.
395 function getSubmittedRealName() {
396 return $this->data_array['submitted_realname'];
400 * getSubmittedUnixName - get login name of submitter.
402 * @return string The unix name of submitter.
404 function getSubmittedUnixName() {
405 return $this->data_array['submitted_unixname'];
409 * getAssignedTo - get ID of assignee.
411 * @return int user_id of assignee.
413 function getAssignedTo() {
414 return $this->data_array['assigned_to'];
418 * getAssignedEmail - get email of assignee.
420 * @return string The email of assignee.
422 function getAssignedEmail() {
423 return $this->data_array['assigned_email'];
427 * getAssignedRealName - get real name of assignee.
429 * @return string The real name of assignee.
431 function getAssignedRealName() {
432 return $this->data_array['assigned_realname'];
436 * getAssignedUnixName - get login name of assignee.
438 * @return string The unix name of assignee.
440 function getAssignedUnixName() {
441 return $this->data_array['assigned_unixname'];
445 * getOpenDate - get unix time of creation.
447 * @return int unix time.
449 function getOpenDate() {
450 return $this->data_array['open_date'];
454 * getCloseDate - get unix time of closure.
456 * @return int unix time.
458 function getCloseDate() {
459 return $this->data_array['close_date'];
463 * getLastModifiedDate - the last_modified_date of this task.
465 * @return int the last_modified_date.
467 function getLastModifiedDate() {
468 return $this->data_array['last_modified_date'];
472 * getSummary - get text summary of artifact.
474 * @return string The summary (subject).
476 function getSummary() {
477 return $this->data_array['summary'];
481 * getDetails - get text body (message) of artifact.
483 * @return string The body (message).
485 function getDetails() {
486 return $this->data_array['details'];
490 * delete - delete this tracker and all its related data.
492 * @param bool I'm Sure.
493 * @return bool true/false;
495 function delete($sure) {
497 $this->setMissingParamsError();
500 if (!$this->ArtifactType->userIsAdmin()) {
501 $this->setPermissionDeniedError();
505 $res = db_query_params ('DELETE FROM artifact_extra_field_data WHERE artifact_id=$1',
506 array ($this->getID())) ;
508 $this->setError('Error deleting extra field data: '.db_error());
512 $res = db_query_params ('DELETE FROM artifact_file WHERE artifact_id=$1',
513 array ($this->getID())) ;
515 $this->setError('Error deleting file from db: '.db_error());
519 $res = db_query_params ('DELETE FROM artifact_message WHERE artifact_id=$1',
520 array ($this->getID())) ;
522 $this->setError('Error deleting message: '.db_error());
526 $res = db_query_params ('DELETE FROM artifact_history WHERE artifact_id=$1',
527 array ($this->getID())) ;
529 $this->setError('Error deleting history: '.db_error());
533 $res = db_query_params ('DELETE FROM artifact_monitor WHERE artifact_id=$1',
534 array ($this->getID())) ;
536 $this->setError('Error deleting monitor: '.db_error());
540 $res = db_query_params ('DELETE FROM artifact WHERE artifact_id=$1',
541 array ($this->getID())) ;
543 $this->setError('Error deleting artifact: '.db_error());
548 if ($this->getStatusID() == 1) {
549 $res = db_query_params ('UPDATE artifact_counts_agg SET count=count-1,open_count=open_count-1
550 WHERE group_artifact_id=$1',
551 array ($this->getID())) ;
553 $this->setError('Error updating artifact_counts_agg (1): '.db_error());
557 } elseif ($this->getStatusID() == 2) {
558 $res = db_query_params ('UPDATE artifact_counts_agg SET count=count-1
559 WHERE group_artifact_id=$1',
560 array ($this->getID())) ;
562 $this->setError('Error updating artifact_counts_agg (2): '.db_error());
573 * setMonitor - user can monitor this artifact.
575 * @return false - always false - always use the getErrorMessage() for feedback
577 function setMonitor() {
578 if (session_loggedin()) {
580 $user_id=user_getid();
581 $user =& user_get_object(user_getid());
584 //we don't want to include the "And email=" because
585 //a logged-in user's email may have changed
590 $this->setError(_('SetMonitor::Valid Email Address Required'));
595 $res = db_query_params ('SELECT * FROM artifact_monitor WHERE artifact_id=$1 AND user_id=$2',
596 array ($this->getID(),
599 if (!$res || db_numrows($res) < 1) {
601 $res = db_query_params ('INSERT INTO artifact_monitor (artifact_id,user_id) VALUES ($1,$2)',
602 array ($this->getID(),
605 $this->setError(db_error());
608 $this->setError(_('Now Monitoring Artifact'));
612 //already monitoring - remove their monitor
613 db_query_params ('DELETE FROM artifact_monitor
616 array ($this->getID(),
618 $this->setError(_('Artifact Monitoring Deactivated'));
623 function isMonitoring() {
624 if (!session_loggedin()) {
627 $result = db_query_params ('SELECT count(*) AS count FROM artifact_monitor WHERE user_id=$1 AND artifact_id=$2',
630 $row_count = db_fetch_array($result);
631 return $result && $row_count['count'] > 0;
635 * getMonitorIds - array of email addresses monitoring this Artifact.
637 * @return array of email addresses monitoring this Artifact.
639 function getMonitorIds() {
640 $res = db_query_params ('SELECT user_id FROM artifact_monitor WHERE artifact_id=$1',
641 array ($this->getID())) ;
642 return array_unique(array_merge($this->ArtifactType->getMonitorIds(),util_result_column_to_array($res)));
646 * getHistory - returns a result set of audit trail for this support request.
648 * @return database result set.
650 function getHistory() {
651 return db_query_params ('SELECT * FROM artifact_history_user_vw WHERE artifact_id=$1 ORDER BY entrydate DESC',
652 array ($this->getID())) ;
656 * getMessages - get the list of messages attached to this artifact.
658 * @return database result set.
660 function getMessages() {
661 return db_query_params ('SELECT * FROM artifact_message_user_vw WHERE artifact_id=$1 ORDER BY adddate DESC',
662 array ($this->getID())) ;
666 * getMessageObjects - get an array of message objects.
668 * @return array Of ArtifactMessage objects.
670 function &getMessageObjects() {
671 $res=$this->getMessages();
673 while ($arr = db_fetch_array($res)) {
674 //$return[]=new ArtifactMessage($arr['artifact_id'],$arr);
675 $return[] = new ArtifactMessage($this, $arr);
681 * getFiles - get array of ArtifactFile's.
683 * @return array of ArtifactFile's.
685 function &getFiles() {
686 if (!isset($this->files)) {
687 $res = db_query_params ('SELECT id,artifact_id,description,filename,filesize," .
688 "filetype,adddate,submitted_by,user_name,realname
689 FROM artifact_file_user_vw WHERE artifact_id=$1',
690 array ($this->getID())) ;
691 $rows=db_numrows($res);
693 for ($i=0; $i < $rows; $i++) {
694 $this->files[$i]=new ArtifactFile($this,db_fetch_array($res));
697 $this->files=array();
704 * getRelatedTasks - get array of related tasks
706 * @return Database result set
708 function getRelatedTasks() {
709 if (!$this->relatedtasks) {
710 $this->relatedtasks = db_query_params ('SELECT pt.group_project_id,pt.project_task_id,pt.summary,pt.start_date,pt.end_date,pgl.group_id, pt.status_id
711 FROM project_task pt, project_group_list pgl
712 WHERE pt.group_project_id = pgl.group_project_id AND
713 EXISTS (SELECT project_task_id FROM project_task_artifact
714 WHERE project_task_id=pt.project_task_id
715 AND artifact_id = $1)',
716 array ($this->getID())) ;
718 return $this->relatedtasks;
722 * addMessage - attach a text message to this Artifact.
724 * @param string The message being attached.
725 * @param string Email address of message creator.
726 * @param bool Whether to email out a followup.
728 * @return boolean success.
730 function addMessage($body,$by=false,$send_followup=false) {
732 $this->setMissingParamsError();
735 if (session_loggedin()) {
736 $user_id=user_getid();
737 $user =& user_get_object($user_id);
738 if (!$user || !is_object($user)) {
739 $this->setError('ERROR - Logged In User Bug Could Not Get User Object');
742 // we'll store this email even though it will likely never be used -
743 // since we have their correct user_id, we can join the USERS table to get email
744 $by=$user->getEmail();
745 } elseif (!$this->ArtifactType->allowsAnon()) {
746 $this->setError(_('Artifact: This ArtifactType Does Not Allow Anonymous Submissions. Please Login.'));
750 if (!$by || !validate_email($by)) {
751 $this->setMissingParamsError();
756 $res = db_query_params ('INSERT INTO artifact_message (artifact_id,submitted_by,from_email,adddate,body) VALUES ($1,$2,$3,$4,$5)',
757 array ($this->getID(),
761 htmlspecialchars($body))) ;
762 if ($send_followup) {
763 $this->mailFollowup(2,false);
769 * addHistory - add an entry to audit trail.
771 * @param string The name of the field in the database being modified.
772 * @param string The former value of this field.
774 * @return boolean success.
776 function addHistory($field_name,$old_value) {
777 if (!session_loggedin()) {
782 return db_query_params ('INSERT INTO artifact_history(artifact_id,field_name,old_value,mod_by,entrydate) VALUES ($1,$2,$3,$4,$5)',
783 array ($this->getID(),
785 addslashes($old_value),
791 * update - update the fields in this artifact.
793 * @param int The artifact priority.
794 * @param int The artifact status ID.
795 * @param int The person to which this artifact is to be assigned.
796 * @param string The artifact summary.
797 * @param int The canned response.
798 * @param string Attaching another comment.
799 * @param int Allows you to move an artifact to another type.
800 * @param array Array of extra fields like: array(15=>'foobar',22=>'1');
801 * @param string The description.
802 * @return boolean success.
804 function update($priority,$status_id,
805 $assigned_to,$summary,$canned_response,$details,$new_artifact_type_id,
806 $extra_fields=array(), $description='') {
809 Field-level permission checking
811 if ($this->ArtifactType->userIsAdmin()) {
812 //admin can do everything
814 //everyone else cannot modify these fields
815 $priority=$this->getPriority();
816 $summary=addslashes($this->getSummary());
817 $description=addslashes($this->getDetails());
818 $canned_response=100;
819 $new_artifact_type_id=$this->ArtifactType->getID();
820 $assigned_to=$this->getAssignedTo();
822 if ($this->ArtifactType->userIsTechnician()) {
823 //technician can update only certain fields
824 //which were not overridden above
826 //submitter can no longer call this function
827 $this->setPermissionDeniedError();
833 // They may be using an extra field "status" box so we have to remap
834 // the status_id based on the extra field - this keeps the counters
837 if (count($extra_fields) > 0) {
838 $status_id=$this->ArtifactType->remapStatus($status_id,$extra_fields);
844 || !$new_artifact_type_id) {
845 $this->setMissingParamsError();
850 // Check that assigned_to is member of the project.
851 if ($assigned_to != 100) {
852 $res = $this->ArtifactType->getTechnicians();
853 $arr =& util_result_column_to_array($res,0);
854 if (!in_array($assigned_to, $arr)) {
855 $this->setError("Invalid assigned_to (not member of the project)");
860 // Array to record which properties were changed
867 // Get a lock on this row in the database
869 $lock = db_query_params ('SELECT * FROM artifact WHERE artifact_id=$1 FOR UPDATE',
870 array ($this->getID())) ;
871 $artifact_type_id = $this->ArtifactType->getID();
873 // Attempt to move this Artifact to a new ArtifactType
874 // need to instantiate new ArtifactType obj and test perms
876 if ($new_artifact_type_id != $artifact_type_id) {
877 $newArtifactType= new ArtifactType($this->ArtifactType->getGroup(), $new_artifact_type_id);
878 if (!is_object($newArtifactType) || $newArtifactType->isError()) {
879 $this->setError('Artifact: Could not move to new ArtifactType'. $newArtifactType->getErrorMessage());
883 // do they have perms for new ArtifactType?
884 if (!$newArtifactType->userIsAdmin()) {
885 $this->setPermissionDeniedError();
890 // Add a message to explain that the tracker was moved.
891 $message = 'Moved from '.$this->ArtifactType->getName().' to '.$newArtifactType->getName();
892 $this->addHistory('type', $this->ArtifactType->getName());
893 $this->addMessage($message,'',0);
895 // Fake change to send a mail when moved.
896 $changes['Type'] = 1;
898 // Try to remap extra_fields values when possible.
899 // If there is an extra_field with the same alias
900 // and if the value exist in the new one, then recode
901 // the value to keep it.
902 $new_extra_fields = array();
903 $ef = $this->ArtifactType->getExtraFields();
904 $ef_new = $newArtifactType->getExtraFields();
905 foreach($extra_fields as $extra_id => $value) {
906 $alias = $ef[$extra_id]['alias'];
907 $type = $ef[$extra_id]['field_type'];
909 // Search if there is an extra field with the same alias.
911 foreach($ef_new as $id => $arr) {
912 if ($arr['alias'] == $alias) {
917 // If we found one, copy for simple fields or
918 // search if there is the same value.
920 if ($type == ARTIFACT_EXTRAFIELDTYPE_TEXT ||
921 $type == ARTIFACT_EXTRAFIELDTYPE_INTEGER ||
922 $type == ARTIFACT_EXTRAFIELDTYPE_TEXTAREA ||
923 $type == ARTIFACT_EXTRAFIELDTYPE_RELATION) {
924 $new_extra_fields[$new_id] = $value;
926 $values = $newArtifactType->getExtraFieldElements($new_id);
927 if (is_array($value)) {
928 foreach($value as $v) {
929 $v = $this->ArtifactType->getElementName($v);
930 foreach($values as $ev) {
931 if ($ev['element_name'] == $v) {
932 $new_extra_fields[$new_id][] = $ev['element_id'];
937 $value = $this->ArtifactType->getElementName($value);
938 foreach($values as $ev) {
939 if ($ev['element_name'] == $value) {
940 $new_extra_fields[$new_id] = $ev['element_id'];
948 // Special case if moving to a tracker with custom status (previous has not).
949 $custom_status_id = $newArtifactType->getCustomStatusField();
950 if ($custom_status_id && !$new_extra_fields[$custom_status_id]) {
951 $atw = new ArtifactWorkflow($newArtifactType, $custom_status_id);
952 $nodes = $atw->getNextNodes(100);
954 $new_extra_fields[$custom_status_id] = $nodes[0];
957 $extra_fields = $new_extra_fields;
959 $res = db_query_params ('DELETE FROM artifact_extra_field_data WHERE artifact_id=$1',
960 array ($this->getID()));
962 $this->setError('Removal of old artifact_extra_field_data failed: '.db_error());
967 // Check that assigned_to is member of the project.
968 if ($assigned_to != 100) {
969 $res = $newArtifactType->getTechnicians();
970 $arr =& util_result_column_to_array($res,0);
971 if (!in_array($assigned_to, $arr)) {
976 //can't send a canned response when changing ArtifactType
977 $canned_response=100;
978 $this->ArtifactType =& $newArtifactType;
983 // handle audit trail
985 $close_date = $this->getCloseDate();
986 if ($this->getStatusID() != $status_id) {
987 $this->addHistory('status_id',$this->getStatusID());
988 $changes['status'] = 1;
992 // Enter the timestamp if we are changing to closed
994 if ($status_id != 1) {
995 $sqlu .= " close_date='".time()."', ";
997 // Reset the close_date if bug is re-opened
998 // (otherwise stat reports will be wrong).
999 $sqlu .= " close_date='0', ";
1001 $this->addHistory('close_date', $this->getCloseDate());
1003 if ($this->getPriority() != $priority) {
1004 $this->addHistory('priority',$this->getPriority());
1005 $changes['priority'] = 1;
1009 if ($this->getAssignedTo() != $assigned_to) {
1010 $this->addHistory('assigned_to',$this->getAssignedTo());
1011 $changes['assigned_to'] = 1;
1014 if ($summary && ($this->getSummary() != htmlspecialchars(stripslashes($summary)))) {
1015 $this->addHistory('summary', $this->getSummary());
1016 $changes['summary'] = 1;
1019 if ($description && ($this->getDetails() != htmlspecialchars(stripslashes($description)))) {
1020 $this->addHistory('details', $this->getDetails());
1021 $changes['details'] = 1;
1025 $this->addMessage($details,'',0);
1026 $changes['details'] = 1;
1031 Finally, update the artifact itself
1034 $result = db_query_params ('UPDATE artifact
1042 group_artifact_id=$7
1044 artifact_id=$8 AND group_artifact_id=$9',
1048 htmlspecialchars($summary),
1049 htmlspecialchars($description),
1051 $new_artifact_type_id,
1053 $artifact_type_id)) ;
1055 if (!$result || db_affected_rows($result) < 1) {
1056 $this->setError('Error - update failed!'.db_error());
1060 if (!$this->fetchData($this->getID())) {
1067 //extra field handling
1069 if (!$this->updateExtraFields($extra_fields,$changes)) {
1070 //TODO - see if anything actually did change
1076 handle canned responses
1078 Instantiate ArtifactCanned and get the body of the message
1080 if ($canned_response != 100) {
1081 //don't care if this response is for this group - could be hacked
1082 $acr=new ArtifactCanned($this->ArtifactType,$canned_response);
1083 if (!$acr || !is_object($acr)) {
1084 $this->setError('Artifact: Could Not Create Canned Response Object');
1085 } elseif ($acr->isError()) {
1086 $this->setError('Artifact: '.$acr->getErrorMessage());
1088 $body = addslashes($acr->getBody());
1090 if (!$this->addMessage(util_unconvert_htmlspecialchars($body),'',0)) {
1097 $this->setError('Artifact: Unable to Use Canned Response');
1103 if ($update || $send_message){
1104 if (!empty($changes)) {
1105 // Send the email with changes
1106 $this->mailFollowup(2, false, $changes);
1111 //nothing changed, so cancel the transaction
1112 $this->setError(_('Nothing Changed - Update Cancelled'));
1120 * updateExtraFields - updates the extra data elements for this artifact
1121 * e.g. the extra fields created and defined by the admin.
1123 * @param array Array of extra fields like: array(15=>'foobar',22=>'1');
1124 * @param array Array where changes to the extra fields should be logged
1125 * @return true on success / false on failure
1127 function updateExtraFields($extra_fields,&$changes){
1129 This is extremely complex code - we have take the passed array
1130 and see if we need to insert it into the db, and may have to
1131 add history rows for the audit trail
1133 start by getting all the available extra fields from ArtifactType
1134 For each field from ArtifacType, check the passed array -
1135 This prevents someone from passing bogus extra field entries - they will be ignored
1136 if the passed entry is blank, may have to force a default value
1137 if the passed array is different from the existing data in db,
1138 delete old entry and insert new entries, along with possible audit trail
1140 skip it and continue to next item
1143 if (empty($extra_fields)) {
1146 //get a list of extra fields for this artifact_type
1147 $ef = $this->ArtifactType->getExtraFields();
1148 $efk=array_keys($ef);
1150 // If there is a status field, then check against the workflow.
1151 for ($i=0; $i<count($efk); $i++) {
1153 $type=$ef[$efid]['field_type'];
1154 if ($type == ARTIFACT_EXTRAFIELDTYPE_STATUS) {
1155 // Get previous value.
1157 $res = db_query_params ('SELECT field_data FROM artifact_extra_field_data
1158 WHERE artifact_id=$1 AND extra_field_id=$2',
1159 array($this->getID(),
1161 $old = (db_numrows($res)>0) ? db_result($res,0,'field_data') : 100;
1162 if ($old != $extra_fields[$efid]) {
1163 $atw = new ArtifactWorkflow($this->ArtifactType, $efid);
1164 if (!$atw->checkEvent($old, $extra_fields[$efid])) {
1165 $this->setError('Workflow error: You are not authorized to change the Status');
1172 //now we'll update this artifact for each extra field
1173 for ($i=0; $i<count($efk); $i++) {
1175 $type=$ef[$efid]['field_type'];
1177 // check required fields
1178 if ($ef[$efid]['is_required']) {
1179 if (!array_key_exists($efid, $extra_fields)) {
1180 if ($type == ARTIFACT_EXTRAFIELDTYPE_STATUS) {
1181 $this->setError('Status Custom Field Must Be Set');
1184 $this->setMissingParamsError($ef[$efid]['field_name']);
1189 if ($extra_fields[$efid] === '') {
1190 if ($type == ARTIFACT_EXTRAFIELDTYPE_STATUS) {
1191 $this->setError('Status Custom Field Must Be Set');
1194 $this->setMissingParamsError($ef[$efid]['field_name']);
1199 if (($type == ARTIFACT_EXTRAFIELDTYPE_SELECT || $type == ARTIFACT_EXTRAFIELDTYPE_RADIO) &&
1200 $extra_fields[$efid] == '100') {
1201 $this->setMissingParamsError($ef[$efid]['field_name']);
1204 elseif (($type == ARTIFACT_EXTRAFIELDTYPE_MULTISELECT || $type == ARTIFACT_EXTRAFIELDTYPE_CHECKBOX) &&
1205 (count($extra_fields[$efid]) == 1 && $extra_fields[$efid][0] == '100')) {
1206 $this->setMissingParamsError($ef[$efid]['field_name']);
1213 // Force each field to have some value if it is a numeric field
1214 // text fields will just be purged and skipped
1216 if (!array_key_exists($efid, $extra_fields) || $extra_fields[$efid] === '') {
1217 if (($type == ARTIFACT_EXTRAFIELDTYPE_SELECT) || ($type == ARTIFACT_EXTRAFIELDTYPE_RADIO)) {
1218 $extra_fields[$efid]='100';
1219 } elseif (($type == ARTIFACT_EXTRAFIELDTYPE_MULTISELECT) || ($type == ARTIFACT_EXTRAFIELDTYPE_CHECKBOX)) {
1220 $extra_fields[$efid]=array('100');
1222 $resdel = db_query_params ('DELETE FROM artifact_extra_field_data WHERE artifact_id=$1 AND extra_field_id=$2',
1223 array ($this->getID(),
1229 // get the old rows of data
1231 $resd = db_query_params ('SELECT * FROM artifact_extra_field_data WHERE artifact_id=$1 AND extra_field_id=$2',
1232 array ($this->getID(),
1234 $rows=db_numrows($resd);
1235 if ($resd && $rows) {
1237 //POTENTIAL PROBLEM - no entry was there before, but adding one now - may need history
1240 // Compare for history purposes
1243 // these types have arrays associated to them, so they need
1244 // special handling to check for differences
1245 if ($type == ARTIFACT_EXTRAFIELDTYPE_MULTISELECT || $type == ARTIFACT_EXTRAFIELDTYPE_CHECKBOX) {
1246 // check the differences between the old values and the new values
1247 $old_values = util_result_column_to_array($resd,"field_data");
1249 $added_values = array_diff($extra_fields[$efid], $old_values);
1250 $deleted_values = array_diff($old_values, $extra_fields[$efid]);
1252 if (!empty($added_values) || !empty($deleted_values)) { // there are differences...
1253 $field_name = $ef[$efid]['field_name'];
1254 if (!preg_match('/^@/', $ef[$efid]['alias'])) {
1255 $changes["extra_fields"][$efid] = 1;
1258 $this->addHistory($field_name, $this->ArtifactType->getElementName(array_reverse($old_values)));
1260 $resdel = db_query_params ('DELETE FROM artifact_extra_field_data WHERE artifact_id=$1 AND extra_field_id=$2',
1261 array ($this->getID(),
1266 } elseif (addslashes(db_result($resd,0,'field_data')) == htmlspecialchars($extra_fields[$efid])) {
1267 //element did not change
1270 //element DID change - do a history entry
1271 $field_name = $ef[$efid]['field_name'];
1272 if (!preg_match('/^@/', $ef[$efid]['alias'])) {
1273 $changes["extra_fields"][$efid] = 1;
1275 $resdel = db_query_params ('DELETE FROM artifact_extra_field_data WHERE artifact_id=$1 AND extra_field_id=$2',
1276 array ($this->getID(),
1279 // Adding history with previous value.
1280 if (($type == ARTIFACT_EXTRAFIELDTYPE_SELECT) || ($type == ARTIFACT_EXTRAFIELDTYPE_RADIO) || ($type == ARTIFACT_EXTRAFIELDTYPE_STATUS)) {
1281 $this->addHistory($field_name,$this->ArtifactType->getElementName(db_result($resd,0,'field_data')));
1283 $this->addHistory($field_name, db_result($resd,0,'field_data'));
1288 //no history for this extra field exists
1293 // Some rewrite & consistency checks on the relation type field.
1295 // 1) Convert syntax [#NNN] to NNN
1296 // 2) Allow multiple spaces as separator.
1297 // 3) Ensure that only integers are given.
1298 // 4) Ensure that id corresponds to valid tracker id.
1300 if ($type == ARTIFACT_EXTRAFIELDTYPE_RELATION) {
1301 $value = preg_replace('/\[\#(\d+)\]/', "\\1", trim($extra_fields[$efid]));
1302 $value = preg_replace('/\\s+/', ' ', $value);
1304 foreach (explode(' ',$value) as $id) {
1305 if (preg_match('/^(\d+)$/', $id)) {
1306 // Control that the id is present in the db
1308 $res = db_query_params ('SELECT artifact_id FROM artifact WHERE artifact_id=$1',
1310 if (db_numrows($res) == 1) {
1313 $this->setError('Illegal id '.$id.', it\'s not a valid tracker id for field: '.$ef[$efid]['field_name'].'.'); // @todo: lang
1317 $this->setError('Illegal value '.$id.', only trackers id are allowed for field: '.$ef[$efid]['field_name'].'.'); // @todo: lang
1321 $extra_fields[$efid] = trim($new);
1324 // Ensure that only integer are allowed for type ARTIFACT_EXTRAFIELDTYPE_INTEGER
1325 if ($type == ARTIFACT_EXTRAFIELDTYPE_INTEGER) {
1326 $extra_fields[$efid] = trim($extra_fields[$efid]);
1327 if (!preg_match('/^[-+]?(\d+)$/', $extra_fields[$efid])) {
1328 $this->setError('Illegal value '.$extra_fields[$efid].' for field '.$ef[$efid]['field_name'].': Only integer is allowed.');
1331 if ($extra_fields[$efid] < -2147483648 || $extra_fields[$efid] > 2147483647) {
1332 $this->setError('Illegal value '.$extra_fields[$efid].' for field '.$ef[$efid]['field_name'].': Integer out of range (-2147483648 to +2147483647).');
1335 $extra_fields[$efid] = intval($extra_fields[$efid]);
1339 // See if anything was even passed for this extra_field_id
1341 if ($extra_fields[$efid] === '') {
1342 //nothing in field to update - text fields may be blank
1344 //determine the type of field and whether it should have multiple rows supporting it
1345 $type=$ef[$efid]['field_type'];
1346 if (($type == ARTIFACT_EXTRAFIELDTYPE_CHECKBOX) || ($type==ARTIFACT_EXTRAFIELDTYPE_MULTISELECT)) {
1348 $count=count($extra_fields[$efid]);
1349 for ($fin=0; $fin<$count; $fin++) {
1350 $res = db_query_params ('INSERT INTO artifact_extra_field_data (artifact_id,extra_field_id,field_data) VALUES ($1,$2,$3)',
1351 array ($this->getID(),
1353 $extra_fields[$efid][$fin])) ;
1355 $this->setError(db_error());
1362 $res = db_query_params ('INSERT INTO artifact_extra_field_data (artifact_id,extra_field_id,field_data) VALUES ($1,$2,$3)',
1363 array ($this->getID(),
1365 htmlspecialchars($extra_fields[$efid]))) ;
1367 $this->setError(db_error());
1373 unset($this->extra_field_data);
1378 * getExtraFieldData - get an array of data for the extra fields associated with this artifact
1380 * @return array array of data
1382 function &getExtraFieldData() {
1383 if (!isset($this->extra_field_data)) {
1384 $this->extra_field_data = array();
1385 $res = db_query_params ('SELECT * FROM artifact_extra_field_data WHERE artifact_id=$1 ORDER BY extra_field_id',
1386 array ($this->getID())) ;
1387 $ef = $this->ArtifactType->getExtraFields();
1388 while ($arr = db_fetch_array($res)) {
1389 $type=$ef[$arr['extra_field_id']]['field_type'];
1390 if (($type == ARTIFACT_EXTRAFIELDTYPE_CHECKBOX) || ($type==ARTIFACT_EXTRAFIELDTYPE_MULTISELECT)) {
1391 //accumulate a sub-array of values in cases where you may have multiple rows
1392 if (!is_array($this->extra_field_data[$arr['extra_field_id']])) {
1393 $this->extra_field_data[$arr['extra_field_id']] = array();
1395 $this->extra_field_data[$arr['extra_field_id']][]=$arr['field_data'];
1397 $this->extra_field_data[$arr['extra_field_id']]=$arr['field_data'];
1401 return $this->extra_field_data;
1405 * marker - adds the > symbol to fields that have been modified for the email message
1409 function marker($prop_name,$changes,$extra_field_id=0) {
1410 if ($prop_name == 'extra_fields' && isset($changes[$prop_name][$extra_field_id])) {
1412 } else if ($prop_name != 'extra_fields' && isset($changes[$prop_name])) {
1420 * mailFollowup - send out an email update for this artifact.
1422 * @param int (1) initial/creation (2) update.
1423 * @param array Array of additional addresses to mail to.
1424 * @param array Array of fields changed in this update .
1426 * @return boolean success.
1428 function mailFollowup($type, $more_addresses=false, $changes='') {
1430 $monitor_ids = array();
1436 $sess = session_get_user() ;
1437 if ($type == 1) { // Initial opening
1439 $body = $this->ArtifactType->getName() ." item #". $this->getID() .", was opened at ". date( _('Y-m-d H:i'), $this->getOpenDate() ) . " by " . $sess->getRealName () ;
1441 $body = $this->ArtifactType->getName() ." item #". $this->getID() .", was opened at ". date( _('Y-m-d H:i'), $this->getOpenDate() ) ;
1445 $body = $this->ArtifactType->getName() ." item #". $this->getID() .", was changed at ". date( _('Y-m-d H:i'), $this->getOpenDate() ) . " by " . $sess->getRealName ();
1447 $body = $this->ArtifactType->getName() ." item #". $this->getID() .", was changed at ". date( _('Y-m-d H:i'), $this->getOpenDate() ) ;
1452 $body .= "\nYou can respond by visiting: ".
1453 "\n".util_make_url ('/tracker/?func=detail&atid='. $this->ArtifactType->getID() .
1454 "&aid=". $this->getID() .
1455 "&group_id=". $this->ArtifactType->Group->getID()) .
1456 "\nOr by replying to this e-mail entering your response between the following markers: ".
1457 "\n".ARTIFACT_MAIL_MARKER.
1458 "\n(enter your response here, only in plain text format)".
1459 "\n".ARTIFACT_MAIL_MARKER.
1461 $this->marker('status',$changes).
1462 "Status: ". $this->getStatusName() ."\n".
1463 $this->marker('priority',$changes).
1464 "Priority: ". $this->getPriority() ."\n".
1465 "Submitted By: ". $this->getSubmittedRealName() .
1466 " (". $this->getSubmittedUnixName(). ")"."\n".
1467 $this->marker('assigned_to',$changes).
1468 "Assigned to: ". $this->getAssignedRealName() .
1469 " (". $this->getAssignedUnixName(). ")"."\n".
1470 $this->marker('summary',$changes).
1471 "Summary: ". util_unconvert_htmlspecialchars( $this->getSummary() )." \n";
1473 // Now display the extra fields
1474 $efd = $this->getExtraFieldDataText();
1475 foreach ($efd as $efid => $ef) {
1476 $body .= $this->marker('extra_fields', $changes, $efid);
1477 $body .= $ef["name"].": ".$ef["value"]."\n";
1480 $subject='['. $this->ArtifactType->Group->getUnixName() . '-' . $this->ArtifactType->getName() . '][' . $this->getID() .'] '. util_unconvert_htmlspecialchars( $this->getSummary() );
1483 // get all the email addresses that are monitoring this request or the ArtifactType
1484 $monitor_ids =& $this->getMonitorIds();
1486 // initial creation, we just get the users monitoring the ArtifactType
1487 $monitor_ids =& $this->ArtifactType->getMonitorIds();
1491 if ($more_addresses) {
1492 $emails[] = $more_addresses;
1494 //we don't email the current user
1495 if ($this->getAssignedTo() != user_getid()) {
1496 $monitor_ids[] = $this->getAssignedTo();
1498 if ($this->getSubmittedBy() != user_getid()) {
1499 $monitor_ids[] = $this->getSubmittedBy();
1501 //initial submission
1503 //if an email is set for this ArtifactType
1504 //add that address to the BCC: list
1505 if ($this->ArtifactType->getEmailAddress()) {
1506 $emails[] = $this->ArtifactType->getEmailAddress();
1510 if ($this->ArtifactType->emailAll()) {
1511 $emails[] = $this->ArtifactType->getEmailAddress();
1515 $body .= "\n\nInitial Comment:".
1516 "\n".util_unconvert_htmlspecialchars( $this->getDetails() ) .
1517 "\n\n----------------------------------------------------------------------";
1521 Now include the followups
1523 $result2=$this->getMessages();
1525 $rows=db_numrows($result2);
1527 if ($result2 && $rows > 0) {
1528 for ($i=0; $i<$rows; $i++) {
1530 // for messages posted by non-logged-in users,
1531 // we grab the email they gave us
1533 // otherwise we use the confirmed one from the users table
1535 if (db_result($result2,$i,'user_id') == 100) {
1536 $emails[] = db_result($result2,$i,'from_email');
1538 $monitor_ids[] = db_result($result2,$i,'user_id');
1544 $body .= $this->marker('details',$changes);
1546 $body .= "Comment By: ". db_result($result2,$i,'realname') . " (".db_result($result2,$i,'user_name').")".
1547 "\nDate: ". date( _('Y-m-d H:i'),db_result($result2,$i,'adddate') ).
1549 "\n".util_unconvert_htmlspecialchars( db_result($result2,$i,'body') ).
1550 "\n\n----------------------------------------------------------------------";
1556 $body .= "\n\nYou can respond by visiting: ".
1557 "\n".util_make_url ('/tracker/?func=detail&atid='. $this->ArtifactType->getID() .
1558 "&aid=". $this->getID() .
1559 "&group_id=". $this->ArtifactType->Group->getID());
1561 //only send if some recipients were found
1562 if (count($emails) < 1 && count($monitor_ids) < 1) {
1566 if (count($monitor_ids) < 1) {
1567 $monitor_ids=array();
1569 $monitor_ids=array_unique($monitor_ids);
1572 $from = $this->ArtifactType->getReturnEmailAddress();
1573 $extra_headers = 'Reply-to: '.$from;
1575 // load the e-mail addresses of the users
1576 $users =& user_get_objects($monitor_ids);
1577 if (count($users) > 0) {
1578 foreach ($users as $user) {
1579 if ($user->getStatus() == "A") { //we are only sending emails to active users
1580 $emails[] = $user->getEmail();
1585 //now remove all duplicates from the email list
1586 if (count($emails) > 0) {
1587 $BCC=implode(',',array_unique($emails));
1588 util_send_message('',$subject,$body,$from,$BCC,'',$extra_headers);
1592 //util_handle_message($monitor_ids,$subject,$body,$BCC);
1598 * getExtraFieldDataText - Return the extra fields' data in a human-readable form.
1600 * @return array Array containing field ID => field name and value associated to it for
1603 function getExtraFieldDataText() {
1604 // First we get the list of extra fields and the data
1605 // associated to the fields
1606 $efs = $this->ArtifactType->getExtraFields();
1607 $efd = $this->getExtraFieldData();
1611 foreach ($efs as $efid => $ef) {
1612 $name = $ef["field_name"];
1613 $type = $ef["field_type"];
1615 // Get the value according to the type
1618 // for these types, the associated value comes straight
1619 case ARTIFACT_EXTRAFIELDTYPE_TEXT:
1620 case ARTIFACT_EXTRAFIELDTYPE_TEXTAREA:
1621 case ARTIFACT_EXTRAFIELDTYPE_RELATION:
1622 case ARTIFACT_EXTRAFIELDTYPE_INTEGER:
1623 if (isset($efd[$efid])) {
1624 $value = $efd[$efid];
1630 // the other types have and ID or an array of IDs associated to them
1632 if (isset($efd[$efid])) {
1633 $value = $this->ArtifactType->getElementName($efd[$efid]);
1639 $return[$efid] = array("name" => $name, "value" => $value, 'type' => $type);
1649 // c-file-style: "bsd"