95bc4c4096f5b6800b3c7bc6f2249a3c8f2cf5fa
[freeside.git] / FS / FS / log.pm
1 package FS::log;
2
3 use strict;
4 use base qw( FS::Record );
5 use FS::Record qw( qsearch qsearchs dbdef );
6 use FS::UID qw( dbh driver_name );
7 use FS::log_context;
8 use FS::log_email;
9
10 =head1 NAME
11
12 FS::log - Object methods for log records
13
14 =head1 SYNOPSIS
15
16   use FS::log;
17
18   $record = new FS::log \%hash;
19   $record = new FS::log { 'column' => 'value' };
20
21   $error = $record->insert;
22
23   $error = $new_record->replace($old_record);
24
25   $error = $record->delete;
26
27   $error = $record->check;
28
29 =head1 DESCRIPTION
30
31 An FS::log object represents a log entry.  FS::log inherits from
32 FS::Record.  The following fields are currently supported:
33
34 =over 4
35
36 =item lognum - primary key
37
38 =item _date - Unix timestamp
39
40 =item agentnum - L<FS::agent> to which the log pertains.  If it involves a 
41 specific customer, package, service, invoice, or other agent-specific object,
42 this will be set to that agentnum.
43
44 =item tablename - table name to which the log pertains, if any.
45
46 =item tablenum - foreign key to that table.
47
48 =item level - log level: 'debug', 'info', 'notice', 'warning', 'error', 
49 'critical', 'alert', 'emergency'.
50
51 =item message - contents of the log entry
52
53 =back
54
55 =head1 METHODS
56
57 =over 4
58
59 =item new HASHREF
60
61 Creates a new log entry.  Use FS::Log instead of calling this directly, 
62 please.
63
64 =cut
65
66 sub table { 'log'; }
67
68 =item insert [ CONTEXT... ]
69
70 Adds this record to the database.  If there is an error, returns the error,
71 otherwise returns false.
72
73 CONTEXT may be a list of context tags to attach to this record.
74
75 Will send emails according to the conditions in L<FS::log_email>.
76
77 =cut
78
79 sub insert {
80   # not using process_o2m for this, because we don't have a web interface
81   my $self = shift;
82   my $error = $self->SUPER::insert;
83   return $error if $error;
84   my $contexts = {}; #for quick checks when sending emails
85   foreach ( @_ ) {
86     my $context = FS::log_context->new({
87         'lognum'  => $self->lognum,
88         'context' => $_
89     });
90     $error = $context->insert;
91     return $error if $error;
92     $contexts->{$_} = 1;
93   }
94   foreach my $log_email (
95     qsearch('log_email',
96       {
97         'disabled' => '',
98         'min_level' => {
99           'op' => '<=',
100           'value' => $self->level,
101         },
102       }
103     )
104   ) {
105     # shouldn't be a lot of these, so not packing this into the qsearch
106     next if $log_email->context && !$contexts->{$log_email->context};
107     my $msg_template = qsearchs('msg_template',{ 'msgnum' => $log_email->msgnum });
108     unless ($msg_template) {
109       warn "Could not send email when logging, could not load message template for logemailnum " . $log_email->logemailnum;
110       next;
111     }
112     my $emailerror = $msg_template->send(
113       'msgtype'       => 'admin',
114       'to'            => $log_email->to_addr,
115       'substitutions' => {
116         'loglevel'   => $FS::Log::LEVELS[$self->level], # which has hopefully been loaded...
117         'logcontext' => $log_email->context, # use the one that triggered the email
118         'logmessage' => $self->message,
119       },
120     );
121     warn "Could not send email when logging: $emailerror" if $emailerror;
122   }
123   '';
124 }
125
126 # these methods can be inherited from FS::Record
127
128 sub delete  { die "Log entries can't be modified." };
129
130 sub replace { die "Log entries can't be modified." };
131
132 =item check
133
134 Checks all fields to make sure this is a valid example.  If there is
135 an error, returns the error, otherwise returns false.  Called by the insert
136 and replace methods.
137
138 =cut
139
140 sub check {
141   my $self = shift;
142
143   my $error = 
144     $self->ut_numbern('lognum')
145     || $self->ut_number('_date')
146     || $self->ut_numbern('agentnum')
147     || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
148     || $self->ut_textn('tablename')
149     || $self->ut_numbern('tablenum')
150     || $self->ut_number('level')
151     || $self->ut_anything('message')
152   ;
153   return $error if $error;
154
155   if ( my $tablename = $self->tablename ) {
156     my $dbdef_table = dbdef->table($tablename)
157       or return "tablename '$tablename' does not exist";
158     $error = $self->ut_foreign_key('tablenum',
159                                    $tablename,
160                                    $dbdef_table->primary_key);
161     return $error if $error;
162   }
163
164   $self->SUPER::check;
165 }
166
167 =item context
168
169 Returns the context for this log entry, as an array, from least to most
170 specific.
171
172 =cut
173
174 sub context {
175   my $self = shift;
176   map { $_->context } qsearch({
177       table     => 'log_context',
178       hashref   => { lognum => $self->lognum },
179       order_by  => 'ORDER BY logcontextnum ASC',
180   });
181 }
182
183 =back
184
185 =head1 CLASS METHODS
186
187 =over 4
188
189 =item search HASHREF
190
191 Returns a qsearch hash expression to search for parameters specified in 
192 HASHREF.  Valid parameters are:
193
194 =over 4
195
196 =item agentnum
197
198 =item date - arrayref of start and end date
199
200 =item level - either a specific level, or an arrayref of min and max level
201
202 =item context - a context string that the log entry must have.  This may 
203 change in the future to allow searching for combinations of context strings.
204
205 =item object - any database object, to find log entries related to it.
206
207 =item tablename, tablenum - alternate way of specifying 'object'.
208
209 =item custnum - a customer number, to find log entries related to the customer
210 or any of their subordinate objects (invoices, packages, etc.).
211
212 =item message - a text string to search in messages.  The search will be 
213 a case-insensitive LIKE with % appended at both ends.
214
215 =back
216
217 =cut
218
219 # used for custnum search: all tables with custnums
220 my @table_stubs;
221
222 sub _setup_table_stubs {
223   foreach my $table (
224     qw( 
225     contact
226     cust_attachment
227     cust_bill
228     cust_credit
229     cust_location
230     cust_main
231     cust_main_exemption
232     cust_main_note
233     cust_msg
234     cust_pay
235     cust_pay_batch
236     cust_pay_pending
237     cust_pay_void
238     cust_pkg
239     cust_refund
240     cust_statement
241     cust_tag
242     cust_tax_adjustment
243     cust_tax_exempt
244     did_order_item
245     qual
246     queue ) )
247   {
248     my $pkey = dbdef->table($table)->primary_key;
249     push @table_stubs,
250       "log.tablename = '$table' AND ".
251       "EXISTS(SELECT 1 FROM $table WHERE log.tablenum = $table.$pkey AND ".
252       "$table.custnum = "; # needs a closing )
253   }
254   # plus this case
255   push @table_stubs,
256       "(log.tablename LIKE 'svc_%' OR log.tablename = 'cust_svc') AND ".
257       "EXISTS(SELECT 1 FROM cust_svc JOIN cust_pkg USING (svcnum) WHERE ".
258       "cust_pkg.custnum = "; # needs a closing )
259 }
260
261 sub search {
262   my ($class, $params) = @_;
263   my @where;
264
265   ##
266   # parse agent
267   ##
268
269   if ( $params->{'agentnum'} =~ /^(\d+)$/ ) {
270     push @where,
271       "log.agentnum = $1";
272   }
273
274   ##
275   # parse custnum
276   ##
277
278   if ( $params->{'custnum'} =~ /^(\d+)$/ ) {
279     _setup_table_stubs() unless @table_stubs;
280     my $custnum = $1;
281     my @orwhere = map { "( $_ $custnum) )" } @table_stubs;
282     push @where, join(' OR ', @orwhere);
283   }
284
285   ##
286   # parse level
287   ##
288
289   if ( ref $params->{'level'} eq 'ARRAY' ) {
290     my ($min, $max) = @{ $params->{'level'} };
291     if ( $min =~ /^\d+$/ ) {
292       push @where, "log.level >= $min";
293     }
294     if ( $max =~ /^\d+$/ ) {
295       push @where, "log.level <= $max";
296     }
297   } elsif ( $params->{'level'} =~ /^(\d+)$/ ) {
298     push @where, "log.level = $1";
299   }
300
301   ##
302   # parse date
303   ##
304
305   if ( ref $params->{'date'} eq 'ARRAY' ) {
306     my ($beg, $end) = @{ $params->{'date'} };
307     if ( $beg =~ /^\d+$/ ) {
308       push @where, "log._date >= $beg";
309     }
310     if ( $end =~ /^\d+$/ ) {
311       push @where, "log._date <= $end";
312     }
313   }
314
315   ##
316   # parse object
317   ##
318
319   if ( $params->{'object'} and $params->{'object'}->isa('FS::Record') ) {
320     my $table = $params->{'object'}->table;
321     my $pkey = dbdef->table($table)->primary_key;
322     my $tablenum = $params->{'object'}->get($pkey);
323     if ( $table and $tablenum ) {
324       push @where, "log.tablename = '$table'", "log.tablenum = $tablenum";
325     }
326   } elsif ( $params->{'tablename'} =~ /^(\w+)$/ ) {
327     my $table = $1;
328     if ( $params->{'tablenum'} =~ /^(\d+)$/ ) {
329       push @where, "log.tablename = '$table'", "log.tablenum = $1";
330     }
331   }
332
333   ##
334   # parse message
335   ##
336
337   if ( $params->{'message'} ) { # can be anything, really, so escape it
338     my $quoted_message = dbh->quote('%' . $params->{'message'} . '%');
339     my $op = (driver_name eq 'Pg' ? 'ILIKE' : 'LIKE');
340     push @where, "log.message $op $quoted_message";
341   }
342
343   ##
344   # parse context
345   ##
346
347   if ( $params->{'context'} ) {
348     my $quoted = dbh->quote($params->{'context'});
349     push @where, 
350       "EXISTS(SELECT 1 FROM log_context WHERE log.lognum = log_context.lognum ".
351       "AND log_context.context = $quoted)";
352   }
353
354   # agent virtualization
355   my $access_user = $FS::CurrentUser::CurrentUser;
356   push @where, $access_user->agentnums_sql(
357     table => 'log',
358     viewall_right => 'Configuration',
359     null => 1,
360   );
361
362   # put it together
363   my $extra_sql = '';
364   $extra_sql .= 'WHERE ' . join(' AND ', @where) if @where;
365   my $count_query = 'SELECT COUNT(*) FROM log '.$extra_sql;
366   my $sql_query = {
367     'table'         => 'log',
368     'hashref'       => {},
369     'select'        => 'log.*',
370     'extra_sql'     => $extra_sql,
371     'count_query'   => $count_query,
372     'order_by'      => 'ORDER BY _date ASC',
373     #addl_from, not needed
374   };
375 }
376
377 =back
378
379 =head1 BUGS
380
381 =head1 SEE ALSO
382
383 L<FS::Record>, schema.html from the base documentation.
384
385 =cut
386
387 1;
388