1 package FS::TicketSystem;
4 use vars qw( $conf $system $AUTOLOAD );
6 use FS::UID qw( dbh driver_name );
7 use FS::Record qw( dbdef );
9 FS::UID->install_callback( sub {
11 $system = $conf->config('ticket_system');
20 my $conf = new FS::Conf;
21 die "FS::TicketSystem::$AUTOLOAD called, but no ticket system configured\n"
24 eval "use FS::TicketSystem::$system;";
34 WillResolve => { type => 'timestamp', null => 1, default => '', },
37 Required => { type => 'integer', default => 0, null => 0 },
42 my $system = FS::Conf->new->config('ticket_system');
43 return if !defined($system) || $system ne 'RT_Internal';
44 my ($class, %opts) = @_;
48 my $case = driver_name eq 'mysql' ? sub {@_} : sub {map lc, @_};
49 foreach my $tablename (keys %columns) {
50 my $table = dbdef->table(&$case($tablename));
53 "$tablename table does not exist. Your RT installation is incomplete.\n";
56 foreach my $colname (keys %{ $columns{$tablename} }) {
57 if ( !$table->column(&$case($colname)) ) {
58 my $col = new DBIx::DBSchema::Column {
60 name => &$case($colname),
61 %{ $columns{$tablename}->{$colname} }
63 $col->table_obj($table);
64 push @sql, $col->sql_add_column($dbh);
70 warn "Upgrading RT schema:\n";
71 foreach my $statement (@sql) {
73 $dbh->do( $statement )
74 or die "Error: ". $dbh->errstr. "\n executing: $statement";
80 return if !defined($system) || $system ne 'RT_Internal';
81 my ($class, %opts) = @_;
83 # go ahead and use the RT API for this
85 FS::TicketSystem->init;
86 my $session = FS::TicketSystem->session();
87 # bypass RT ACLs--we're going to do lots of things
88 my $CurrentUser = $RT::SystemUser;
92 # selfservice and cron users
93 foreach my $username ('%%%SELFSERVICE_USER%%%', 'fs_daily') {
94 my $User = RT::User->new($CurrentUser);
95 $User->Load($username);
96 if (!defined($User->Id)) {
97 my ($val, $msg) = $User->Create(
101 # any other fields needed?
105 my $Principal = $User->PrincipalObj; # can this ever fail?
106 my @rights = ( qw(ShowTicket SeeQueue ModifyTicket ReplyToTicket
107 CreateTicket SeeCustomField) );
109 next if $Principal->HasRight( 'Right' => $_, Object => $RT::System );
110 my ($val, $msg) = $Principal->GrantRight(
112 'Object' => $RT::System,
118 # EscalateQueue custom field and friends
119 my $CF = RT::CustomField->new($CurrentUser);
120 $CF->Load('EscalateQueue');
121 if (!defined($CF->Id)) {
122 my ($val, $msg) = $CF->Create(
123 'Name' => 'EscalateQueue',
126 'LookupType' => 'RT::Queue',
127 'Description' => 'Escalate to Queue',
128 'ValuesClass' => 'RT::CustomFieldValues::Queues', #magic!
131 my $OCF = RT::ObjectCustomField->new($CurrentUser);
132 ($val, $msg) = $OCF->Create(
133 'CustomField' => $CF->Id,
139 # Load from RT data file
140 our (@Groups, @Users, @ACL, @Queues, @ScripActions, @ScripConditions,
141 @Templates, @CustomFields, @Scrips, @Attributes, @Initial, @Final,
143 my $datafile = '%%%RT_PATH%%%/etc/initialdata';
144 eval { require $datafile };
146 warn "Couldn't load RT data from '$datafile': $@\n(skipping)\n";
150 # Cache existing ScripCondition, ScripAction, and Template IDs.
151 # Complicated because we don't want to just step on multiple IDs
152 # with the same name.
154 my ($class, $hash) = @_;
155 my $search = $class->new($CurrentUser);
157 while ( my $item = $search->Next ) {
158 my $ids = $hash->{lc($item->Name)} ||= [];
159 if ( $item->Creator == 1 ) { # RT::SystemUser
160 unshift @$ids, $item->Id;
163 push @$ids, $item->Id;
168 my (%condition, %action, %template);
169 &$cachify('RT::ScripConditions', \%condition);
170 &$cachify('RT::ScripActions', \%action);
171 &$cachify('RT::Templates', \%template);
172 # $condition{name} = [ ids... ]
173 # with the id of the system-created object first, if there is one
176 my $ScripCondition = RT::ScripCondition->new($CurrentUser);
177 foreach my $sc (@ScripConditions) {
178 # $sc: Name, Description, ApplicableTransTypes, ExecModule, Argument
179 next if exists( $condition{ lc($sc->{Name}) } );
180 my ($val, $msg) = $ScripCondition->Create( %$sc );
182 $condition{ lc($ScripCondition->Name) } = [ $ScripCondition->Id ];
186 my $ScripAction = RT::ScripAction->new($CurrentUser);
187 foreach my $sa (@ScripActions) {
188 # $sa: Name, Description, ExecModule, Argument
189 next if exists( $action{ lc($sa->{Name}) } );
190 my ($val, $msg) = $ScripAction->Create( %$sa );
192 $action{ lc($ScripAction->Name) } = [ $ScripAction->Id ];
196 my $Template = RT::Template->new($CurrentUser);
197 foreach my $t (@Templates) {
198 # $t: Queue, Name, Description, Content
199 next if exists( $template{ lc($t->{Name}) } );
200 my ($val, $msg) = $Template->Create( %$t );
202 $template{ lc($Template->Name) } = [ $Template->Id ];
206 my %scrip; # $scrips{condition}{action}{template} = id
207 my $search = RT::Scrips->new($CurrentUser);
208 $search->Limit(FIELD => 'Queue', VALUE => 0);
209 while (my $item = $search->Next) {
210 my ($c, $a, $t) = map {lc $item->$_->Name}
211 ('ScripConditionObj', 'ScripActionObj', 'TemplateObj');
212 if ( exists $scrip{$c}{$a} and $item->Creator == 1 ) {
213 warn "Deleting duplicate scrip $c $a [$t]\n";
214 my ($val, $msg) = $item->Delete;
215 warn "error deleting scrip: $msg\n" if !$val;
217 elsif ( exists $Delete_Scrips{$c}{$a}{$t} and $item->Creator == 1 ) {
218 warn "Deleting obsolete scrip $c $a [$t]\n";
219 my ($val, $msg) = $item->Delete;
220 warn "error deleting scrip: $msg\n" if !$val;
223 $scrip{$c}{$a} = $item->id;
226 my $Scrip = RT::Scrip->new($CurrentUser);
227 foreach my $s ( @Scrips ) {
228 my $desc = $s->{'Description'};
229 my ($c, $a, $t) = map lc,
230 @{ $s }{'ScripCondition', 'ScripAction', 'Template'};
232 if ( exists($scrip{$c}{$a}) ) {
233 $Scrip->Load( $scrip{$c}{$a} );
234 } else { # need to create it
236 if ( !exists($condition{$c}) ) {
237 warn "ScripCondition '$c' not found.\n";
240 if ( !exists($action{$a}) ) {
241 warn "ScripAction '$a' not found.\n";
244 if ( !exists($template{$t}) ) {
245 warn "Template '$t' not found.\n";
249 ScripCondition => $condition{$c}->[0],
250 ScripAction => $action{$a}->[0],
251 Template => $template{$t}->[0],
253 Description => $desc,
255 warn "Creating scrip: $c $a [$t]\n";
256 my ($val, $msg) = $Scrip->Create(%new_param);
260 # set the Immutable attribute on them if needed
261 if ( !$Scrip->FirstAttribute('Immutable') ) {
263 $Scrip->SetAttribute(Name => 'Immutable', Content => '1');
269 # one-time fix: accumulator fields (support time, etc.) that had values
270 # entered on ticket creation need OCFV records attached to their Create
272 my $sql = 'SELECT first_ocfv.ObjectId, first_ocfv.Created, Content '.
273 'FROM ObjectCustomFieldValues as first_ocfv '.
275 # subquery to get the first OCFV with a certain name for each ticket
276 'SELECT min(ObjectCustomFieldValues.Id) AS Id '.
277 'FROM ObjectCustomFieldValues '.
278 'JOIN CustomFields '.
279 'ON (ObjectCustomFieldValues.CustomField = CustomFields.Id) '.
280 'WHERE ObjectType = \'RT::Ticket\' '.
281 'AND CustomFields.Name = ? '.
283 ') AS first_ocfv_id USING (Id) '.
285 # subquery to get the first transaction date for each ticket
286 # other than the Create
287 'SELECT ObjectId, min(Created) AS Created FROM Transactions '.
288 'WHERE ObjectType = \'RT::Ticket\' '.
289 'AND Type != \'Create\' '.
291 ') AS first_txn ON (first_ocfv.ObjectId = first_txn.ObjectId) '.
292 # where the ticket custom field acquired a value before any transactions
293 # on the ticket (i.e. it was set on ticket creation)
294 'WHERE first_ocfv.Created < first_txn.Created '.
295 # and we haven't already fixed the ticket
297 'SELECT 1 FROM Transactions JOIN ObjectCustomFieldValues '.
298 'ON (Transactions.Id = ObjectCustomFieldValues.ObjectId) '.
299 'JOIN CustomFields '.
300 'ON (ObjectCustomFieldValues.CustomField = CustomFields.Id) '.
301 'WHERE ObjectCustomFieldValues.ObjectType = \'RT::Transaction\' '.
302 'AND CustomFields.Name = ? '.
303 'AND Transactions.Type = \'Create\''.
304 'AND Transactions.ObjectType = \'RT::Ticket\''.
305 'AND Transactions.ObjectId = first_ocfv.ObjectId'.
309 # prior to this fix, the only name an accumulate field could possibly have
310 # was "Support time".
311 my $sth = $dbh->prepare($sql);
312 $sth->execute('Support time', 'Support time');
313 my $rows = $sth->rows;
314 warn "Fixing support time on $rows rows...\n" if $rows > 0;
315 while ( my $row = $sth->fetchrow_arrayref ) {
316 my ($tid, $created, $content) = @$row;
317 my $Txns = RT::Transactions->new($CurrentUser);
318 $Txns->Limit(FIELD => 'ObjectId', VALUE => $tid);
319 $Txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
320 $Txns->Limit(FIELD => 'Type', VALUE => 'Create');
321 my $CreateTxn = $Txns->First;
323 my ($val, $msg) = $CreateTxn->AddCustomFieldValue(
324 Field => 'Support time',
326 RecordTransaction => 0,
328 warn "Error setting transaction support time: $msg\n" unless $val;
330 warn "Create transaction not found for ticket $tid.\n";
335 my $cve_2013_3373_sql = q(
336 UPDATE Tickets SET Subject = REPLACE(Subject,E'\n','')
339 #UPDATE Tickets SET Subject = REPLACE(Subject,'\n','');
341 my $cve_2013_3373_sth = $dbh->prepare( $cve_2013_3373_sql)
343 $cve_2013_3373_sth->execute or die $cve_2013_3373_sth->errstr;
345 # Remove dangling customer links, if any
346 my %target_pkey = ('cust_main' => 'custnum', 'cust_svc' => 'svcnum');
347 for my $table (keys %target_pkey) {
348 my $pkey = $target_pkey{$table};
350 "DELETE FROM links WHERE id IN(".
351 "SELECT links.id FROM links LEFT JOIN $table ON (links.target = ".
352 "'freeside://freeside/$table/' || $table.$pkey) ".
353 "WHERE links.target like 'freeside://freeside/$table/%' ".
354 "AND $table.$pkey IS NULL".
356 ) or die $dbh->errstr;
357 warn "Removed $rows dangling ticket-$table links\n" if $rows > 0;