edd3ca8300f9060aafe46223645e9f50587d1b67
[freeside.git] / FS / FS / UI / Web.pm
1 package FS::UI::Web;
2
3 use strict;
4 use vars qw($DEBUG @ISA @EXPORT_OK $me);
5 use Exporter;
6 use Carp qw( confess );
7 use HTML::Entities;
8 use FS::Conf;
9 use FS::Misc::DateTime qw( parse_datetime day_end );
10 use FS::Record qw(dbdef qsearch);
11 use FS::cust_main;  # are sql_balance and sql_date_balance in the right module?
12
13 #use vars qw(@ISA);
14 #use FS::UI
15 #@ISA = qw( FS::UI );
16 @ISA = qw( Exporter );
17
18 @EXPORT_OK = qw( get_page_pref set_page_pref svc_url random_id );
19
20 $DEBUG = 0;
21 $me = '[FS::UID::Web]';
22
23 our $NO_RANDOM_IDS;
24
25 ###
26 # user prefs
27 ###
28
29 =item get_page_pref NAME, TABLENUM
30
31 Returns the user's page preference named NAME for the current page. If the
32 page is a view or edit page or otherwise shows a single record at a time,
33 it should use TABLENUM to link the preference to that record.
34
35 =cut
36
37 sub get_page_pref {
38   my ($prefname, $tablenum) = @_;
39
40   my $m = $HTML::Mason::Commands::m
41     or die "can't get page pref when running outside the UI";
42   # what's more useful: to tie prefs to the base_comp (usually where
43   # code is executing right now), or to the request_comp (approximately the
44   # one in the URL)? not sure.
45   $FS::CurrentUser::CurrentUser->get_page_pref( $m->request_comp->path,
46                                                 $prefname,
47                                                 $tablenum
48                                               );
49 }
50
51 =item set_page_pref NAME, TABLENUM, VALUE
52
53 Sets the user's page preference named NAME for the current page. Use TABLENUM
54 as for get_page_pref.
55
56 If VALUE is an empty string, the preference will be deleted (and
57 C<get_page_pref> will return an empty string).
58
59   my $mypref = set_page_pref('mypref', '', 100);
60
61 =cut
62
63 sub set_page_pref {
64   my ($prefname, $tablenum, $prefvalue) = @_;
65
66   my $m = $HTML::Mason::Commands::m
67     or die "can't set page pref when running outside the UI";
68   $FS::CurrentUser::CurrentUser->set_page_pref( $m->request_comp->path,
69                                                 $prefname,
70                                                 $tablenum,
71                                                 $prefvalue );
72 }
73
74 ###
75 # date parsing
76 ###
77
78 =item parse_beginning_ending CGI [, PREFIX ]
79
80 Parses a beginning/ending date range, as used on many reports. This function
81 recognizes two sets of CGI params: "begin" and "end", the integer timestamp
82 values, and "beginning" and "ending", the user-readable date fields.
83
84 If "begin" contains an integer, that's passed through as the beginning date.
85 Otherwise, "beginning" is passed to L<DateTime::Format::Natural> and turned
86 into an integer. If this fails or it doesn't have a value, zero is used as the
87 beginning date.
88
89 The same happens for "end" and "ending", except that if "ending" contains a
90 date without a time, it gets moved to the end of that day, and if there's no
91 value, the value returned is the highest unsigned 32-bit time value (some time
92 in 2037).
93
94 PREFIX is optionally a string to prepend (with '_' as a delimiter) to the form
95 field names.
96
97 =cut
98
99 use Date::Parse;
100 sub parse_beginning_ending {
101   my($cgi, $prefix) = @_;
102   $prefix .= '_' if $prefix;
103
104   my $beginning = 0;
105   if ( $cgi->param($prefix.'begin') =~ /^(\d+)$/ ) {
106     $beginning = $1;
107   } elsif ( $cgi->param($prefix.'beginning') =~ /^([ 0-9\-\/\:]{1,64})$/ ) {
108     $beginning = parse_datetime($1) || 0;
109   }
110
111   my $ending = 4294967295; #2^32-1
112   if ( $cgi->param($prefix.'end') =~ /^(\d+)$/ ) {
113     $ending = $1 - 1;
114   } elsif ( $cgi->param($prefix.'ending') =~ /^([ 0-9\-\/\:]{1,64})$/ ) {
115     $ending = parse_datetime($1);
116     $ending = day_end($ending) unless $ending =~ /:/;
117   }
118
119   ( $beginning, $ending );
120 }
121
122 =item svc_url
123
124 Returns a service URL, first checking to see if there is a service-specific
125 page to link to, otherwise to a generic service handling page.  Options are
126 passed as a list of name-value pairs, and include:
127
128 =over 4
129
130 =item * m - Mason request object ($m)
131
132 =item * action - The action for which to construct "edit", "view", or "search"
133
134 =item ** part_svc - Service definition (see L<FS::part_svc>)
135
136 =item ** svcdb - Service table
137
138 =item *** query - Query string
139
140 =item *** svc   - FS::cust_svc or FS::svc_* object
141
142 =item ahref - Optional flag, if set true returns <A HREF="$url"> instead of just the URL.
143
144 =back 
145
146 * Required fields
147
148 ** part_svc OR svcdb is required
149
150 *** query OR svc is required
151
152 =cut
153
154   # ##
155   # #required
156   # ##
157   #  'm'        => $m, #mason request object
158   #  'action'   => 'edit', #or 'view'
159   #
160   #  'part_svc' => $part_svc, #usual
161   #   #OR
162   #  'svcdb'    => 'svc_table',
163   #
164   #  'query'    => #optional query string
165   #                # (pass a blank string if you want a "raw" URL to add your
166   #                #  own svcnum to)
167   #   #OR
168   #  'svc'      => $svc_x, #or $cust_svc, it just needs a svcnum
169   #
170   # ##
171   # #optional
172   # ##
173   #  'ahref'    => 1, # if set true, returns <A HREF="$url">
174
175 use FS::CGI qw(rooturl);
176 sub svc_url {
177   my %opt = @_;
178
179   #? return '' unless ref($opt{part_svc});
180
181   my $svcdb = $opt{svcdb} || $opt{part_svc}->svcdb;
182   my $query = exists($opt{query}) ? $opt{query} : $opt{svc}->svcnum;
183   my $url;
184   warn "$me [svc_url] checking for /$opt{action}/$svcdb.cgi component"
185     if $DEBUG;
186   if ( $opt{m}->interp->comp_exists("/$opt{action}/$svcdb.cgi") ) {
187     $url = "$svcdb.cgi?";
188   } elsif ( $opt{m}->interp->comp_exists("/$opt{action}/$svcdb.html") ) {
189     $url = "$svcdb.html?";
190   } else {
191     my $generic = $opt{action} eq 'search' ? 'cust_svc' : 'svc_Common';
192
193     $url = "$generic.html?svcdb=$svcdb;";
194     $url .= 'svcnum=' if $query =~ /^\d+(;|$)/ or $query eq '';
195   }
196
197   my $return = FS::CGI::rooturl(). "$opt{action}/$url$query";
198
199   $return = qq!<A HREF="$return">! if $opt{ahref};
200
201   $return;
202 }
203
204 sub svc_link {
205   my($m, $part_svc, $cust_svc) = @_ or return '';
206   svc_X_link( $part_svc->svc, @_ );
207 }
208
209 sub svc_label_link {
210   my($m, $part_svc, $cust_svc) = @_ or return '';
211   my($svc, $label, $svcdb) = $cust_svc->label;
212   svc_X_link( $label, @_ );
213 }
214
215 sub svc_X_link {
216   my ($x, $m, $part_svc, $cust_svc) = @_ or return '';
217
218   return $x
219    unless $FS::CurrentUser::CurrentUser->access_right('View customer services');
220
221   confess "svc_X_link called without a service ($x, $m, $part_svc, $cust_svc)\n"
222     unless $cust_svc;
223
224   my $ahref = svc_url(
225     'ahref'    => 1,
226     'm'        => $m,
227     'action'   => 'view',
228     'part_svc' => $part_svc,
229     'svc'      => $cust_svc,
230   );
231
232   "$ahref$x</A>";
233 }
234
235 #this probably needs an ACL too...
236 sub svc_export_links {
237   my ($m, $part_svc, $cust_svc) = @_ or return '';
238
239   my $ahref = $cust_svc->export_links;
240
241   join('', @$ahref);
242 }
243
244 sub parse_lt_gt {
245   my($cgi, $field) = (shift, shift);
246   my $table = ( @_ && length($_[0]) ) ? shift.'.' : '';
247
248   my @search = ();
249
250   my %op = ( 
251     'lt' => '<',
252     'gt' => '>',
253   );
254
255   foreach my $op (keys %op) {
256
257     warn "checking for ${field}_$op field\n"
258       if $DEBUG;
259
260     if ( $cgi->param($field."_$op") =~ /^\s*\$?\s*(-?[\d\,\s]+(\.\d\d)?)\s*$/ ) {
261
262       my $num = $1;
263       $num =~ s/[\,\s]+//g;
264       my $search = "$table$field $op{$op} $num";
265       push @search, $search;
266
267       warn "found ${field}_$op field; adding search element $search\n"
268         if $DEBUG;
269     }
270
271   }
272
273   @search;
274
275 }
276
277 ###
278 # cust_main report subroutines
279 ###
280
281 =over 4
282
283 =item cust_header [ CUST_FIELDS_VALUE ]
284
285 Returns an array of customer information headers according to the supplied
286 customer fields value, or if no value is supplied, the B<cust-fields>
287 configuration value.
288
289 =cut
290
291 use vars qw( @cust_fields @cust_colors @cust_styles @cust_aligns );
292
293 sub cust_header {
294
295   warn "FS::UI:Web::cust_header called"
296     if $DEBUG;
297
298   my $conf = new FS::Conf;
299
300   my %header2method = (
301     'Customer'                 => 'name',
302     'Cust. Status'             => 'cust_status_label',
303     'Cust#'                    => 'display_custnum',
304     'Name'                     => 'contact',
305     'Company'                  => 'company',
306
307     # obsolete but might still be referenced in configuration
308     '(bill) Customer'          => 'name',
309     '(service) Customer'       => 'ship_name',
310     '(bill) Name'              => 'contact',
311     '(service) Name'           => 'ship_contact',
312     '(bill) Company'           => 'company',
313     '(service) Company'        => 'ship_company',
314     '(bill) Day phone'         => 'daytime',
315     '(bill) Night phone'       => 'night',
316     '(bill) Fax number'        => 'fax',
317  
318     'Customer'                 => 'name',
319     'Address 1'                => 'bill_address1',
320     'Address 2'                => 'bill_address2',
321     'City'                     => 'bill_city',
322     'State'                    => 'bill_state',
323     'Zip'                      => 'bill_zip',
324     'Country'                  => 'bill_country_full',
325     'Day phone'                => 'daytime', # XXX should use msgcat, but how?
326     'Night phone'              => 'night',   # XXX should use msgcat, but how?
327     'Mobile phone'             => 'mobile',  # XXX should use msgcat, but how?
328     'Fax number'               => 'fax',
329     '(bill) Address 1'         => 'bill_address1',
330     '(bill) Address 2'         => 'bill_address2',
331     '(bill) City'              => 'bill_city',
332     '(bill) State'             => 'bill_state',
333     '(bill) Zip'               => 'bill_zip',
334     '(bill) Country'           => 'bill_country_full',
335     '(bill) Latitude'          => 'bill_latitude',
336     '(bill) Longitude'         => 'bill_longitude',
337     '(service) Address 1'      => 'ship_address1',
338     '(service) Address 2'      => 'ship_address2',
339     '(service) City'           => 'ship_city',
340     '(service) State'          => 'ship_state',
341     '(service) Zip'            => 'ship_zip',
342     '(service) Country'        => 'ship_country_full',
343     '(service) Latitude'       => 'ship_latitude',
344     '(service) Longitude'      => 'ship_longitude',
345     'Invoicing email(s)'       => 'invoicing_list_emailonly_scalar',
346 # FS::Upgrade::upgrade_config removes this from existing cust-fields settings
347 #    'Payment Type'             => 'cust_payby',
348     'Current Balance'          => 'current_balance',
349     'Agent Cust#'              => 'agent_custid',
350     'Agent'                    => 'agent_name',
351     'Agent Cust# or Cust#'     => 'display_custnum',
352     'Advertising Source'       => 'referral',
353   );
354   $header2method{'Cust#'} = 'display_custnum'
355     if $conf->exists('cust_main-default_agent_custid');
356
357 foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) {
358   $header2method{'Contact '.$phone_type->typename.' phone(s)'} = sub {
359     my $self = shift;
360     my $num = $phone_type->phonetypenum;
361
362     my @phones;
363     foreach ($self->contact_list_name_phones) {
364       my $data = [
365         {
366           'data'  => $_->first.' '.$_->last.' '.FS::contact_phone::phonenum_pretty($_),
367         },
368       ];
369       push @phones, $data if $_->phonetypenum eq $phone_type->phonetypenum;
370     }
371   return \@phones;
372   };
373
374 }
375
376   my %header2colormethod = (
377     'Cust. Status' => 'cust_statuscolor',
378   );
379   my %header2style = (
380     'Cust. Status' => 'b',
381   );
382   my %header2align = (
383     'Cust. Status' => 'c',
384     'Cust#'        => 'r',
385   );
386
387   my $cust_fields;
388   my @cust_header;
389   if ( @_ && $_[0] ) {
390
391     warn "  using supplied cust-fields override".
392           " (ignoring cust-fields config file)"
393       if $DEBUG;
394     $cust_fields = shift;
395
396   } else {
397
398     if (    $conf->exists('cust-fields')
399          && $conf->config('cust-fields') =~ /^([\w\. \|\#\(\)]+):?/
400        )
401     {
402       warn "  found cust-fields configuration value"
403         if $DEBUG;
404       $cust_fields = $1;
405     } else { 
406       warn "  no cust-fields configuration value found; using default 'Cust. Status | Customer'"
407         if $DEBUG;
408       $cust_fields = 'Cust. Status | Customer';
409     }
410   
411   }
412
413   @cust_header = split(/ \| /, $cust_fields);
414   @cust_fields = map { $header2method{$_} || $_ } @cust_header;
415   @cust_colors = map { exists $header2colormethod{$_}
416                          ? $header2colormethod{$_}
417                          : ''
418                      }
419                      @cust_header;
420   @cust_styles = map { exists $header2style{$_} ? $header2style{$_} : '' }
421                      @cust_header;
422   @cust_aligns = map { exists $header2align{$_} ? $header2align{$_} : 'l' }
423                      @cust_header;
424
425   #my $svc_x = shift;
426   @cust_header;
427 }
428
429 sub cust_sort_fields {
430   cust_header(@_) if( @_ or !@cust_fields );
431   #inefficientish, but tiny lists and only run once per page
432
433   my @sort_fields;
434   foreach (@cust_fields) {
435     if ($_ eq "custnum") { push @sort_fields, 'custnum'; }
436     elsif ($_ eq "contact" || $_ eq "name") { push @sort_fields, '(last, first)'; }
437     elsif ($_ eq "company") { push @sort_fields, 'company'; }
438     else { push @sort_fields, ''; }
439   }
440   return @sort_fields;
441
442 }
443
444 =item cust_sql_fields [ CUST_FIELDS_VALUE ]
445
446 Returns a list of fields for the SELECT portion of an SQL query.
447
448 As with L<the cust_header subroutine|/cust_header>, the fields returned are
449 defined by the supplied customer fields setting, or if no customer fields
450 setting is supplied, the <B>cust-fields</B> configuration value. 
451
452 =cut
453
454 sub cust_sql_fields {
455
456   my @fields = qw( last first company );
457 #  push @fields, map "ship_$_", @fields;
458
459   cust_header(@_) if( @_ or !@cust_fields );
460   #inefficientish, but tiny lists and only run once per page
461
462   my @location_fields;
463   foreach my $field (qw( address1 address2 city state zip latitude longitude )) {
464     foreach my $pre ('bill_','ship_') {
465       if ( grep { $_ eq $pre.$field } @cust_fields ) {
466         push @location_fields, $pre.'location.'.$field.' AS '.$pre.$field;
467       }
468     }
469   }
470   foreach my $pre ('bill_','ship_') {
471     if ( grep { $_ eq $pre.'country_full' } @cust_fields ) {
472       push @location_fields, $pre.'locationnum';
473     }
474   }
475
476   foreach my $field (qw(daytime night mobile fax )) {
477     push @fields, $field if (grep { $_ eq $field } @cust_fields);
478   }
479   push @fields, 'agent_custid';
480
481   push @fields, 'agentnum' if grep { $_ eq 'agent_name' } @cust_fields;
482
483   my @extra_fields = ();
484   if (grep { $_ eq 'current_balance' } @cust_fields) {
485     push @extra_fields, FS::cust_main->balance_sql . " AS current_balance";
486   }
487
488   push @extra_fields, 'part_referral_x.referral AS referral'
489     if grep { $_ eq 'referral' } @cust_fields;
490
491   map("cust_main.$_", @fields), @location_fields, @extra_fields;
492 }
493
494 =item join_cust_main [ TABLE[.CUSTNUM] ] [ LOCATION_TABLE[.LOCATIONNUM] ]
495
496 Returns an SQL join phrase for the FROM clause so that the fields listed
497 in L</cust_sql_fields> will be available.  Currently joins to cust_main
498 itself, as well as cust_location (under the aliases 'bill_location' and
499 'ship_location') if address fields are needed.  L</cust_header> should have
500 been called already.
501
502 All of these will be left joins; if you want to exclude rows with no linked
503 cust_main record (or bill_location/ship_location), you can do so in the 
504 WHERE clause.
505
506 TABLE is the table containing the custnum field.  If CUSTNUM (a field name
507 in that table) is specified, that field will be joined to cust_main.custnum.
508 Otherwise, this function will assume the field is named "custnum".  If the 
509 argument isn't present at all, the join will just say "USING (custnum)", 
510 which might work.
511
512 As a special case, if TABLE is 'cust_main', only the joins to cust_location
513 will be returned.
514
515 LOCATION_TABLE is an optional table name to use for joining ship_location,
516 in case your query also includes package information and you want the 
517 "service address" columns to reflect package addresses.
518
519 =cut
520
521 sub join_cust_main {
522   my ($cust_table, $location_table) = @_;
523   my ($custnum, $locationnum);
524   ($cust_table, $custnum) = split(/\./, $cust_table);
525   $custnum ||= 'custnum';
526   ($location_table, $locationnum) = split(/\./, $location_table);
527   $locationnum ||= 'locationnum';
528
529   my $sql = '';
530   if ( $cust_table ) {
531     $sql = " LEFT JOIN cust_main ON (cust_main.custnum = $cust_table.$custnum)"
532       unless $cust_table eq 'cust_main';
533   } else {
534     $sql = " LEFT JOIN cust_main USING (custnum)";
535   }
536
537   if ( !@cust_fields or grep /^bill_/, @cust_fields ) {
538
539     $sql .= ' LEFT JOIN cust_location bill_location'.
540             ' ON (bill_location.locationnum = cust_main.bill_locationnum)';
541
542   }
543
544   if ( !@cust_fields or grep /^ship_/, @cust_fields ) {
545
546     if (!$location_table) {
547       $location_table = 'cust_main';
548       $locationnum = 'ship_locationnum';
549     }
550
551     $sql .= ' LEFT JOIN cust_location ship_location'.
552             " ON (ship_location.locationnum = $location_table.$locationnum) ";
553   }
554
555   if ( !@cust_fields or grep { $_ eq 'referral' } @cust_fields ) {
556     $sql .= ' LEFT JOIN (select refnum, referral from part_referral) AS part_referral_x ON (cust_main.refnum = part_referral_x.refnum) ';
557   }
558
559   $sql;
560 }
561
562 =item cust_fields OBJECT [ CUST_FIELDS_VALUE ]
563
564 Given an object that contains fields from cust_main (say, from a
565 JOINed search.  See httemplate/search/svc_* for examples), returns an array
566 of customer information, or "(unlinked)" if this service is not linked to a
567 customer.
568
569 As with L<the cust_header subroutine|/cust_header>, the fields returned are
570 defined by the supplied customer fields setting, or if no customer fields
571 setting is supplied, the <B>cust-fields</B> configuration value. 
572
573 =cut
574
575
576 sub cust_fields {
577   my $record = shift;
578   warn "FS::UI::Web::cust_fields called for $record ".
579        "(cust_fields: @cust_fields)"
580     if $DEBUG > 1;
581
582   #cust_header(@_) unless @cust_fields; #now need to cache to keep cust_fields
583   #                                     #override incase we were passed as a sub
584   
585   my $seen_unlinked = 0;
586
587   map { 
588     if ( $record->custnum ) {
589       warn "  $record -> $_" if $DEBUG > 1;
590       encode_entities( $record->$_(@_) );
591     } else {
592       warn "  ($record unlinked)" if $DEBUG > 1;
593       $seen_unlinked++ ? '' : '(unlinked)';
594     }
595   } @cust_fields;
596 }
597
598 =item cust_fields_subs
599
600 Returns an array of subroutine references for returning customer field values.
601 This is similar to cust_fields, but returns each field's sub as a distinct 
602 element.
603
604 =cut
605
606 sub cust_fields_subs {
607   my $unlinked_warn = 0;
608
609   return map { 
610     my $f = $_;
611     if ( $unlinked_warn++ ) {
612
613       sub {
614         my $record = shift;
615         if ( $record->custnum ) {
616           encode_entities( $record->$f(@_) );
617         } else {
618           '(unlinked)'
619         };
620       };
621
622     } else {
623
624       sub {
625         my $record = shift;
626         $record->custnum ? encode_entities( $record->$f(@_) ) : '';
627       };
628
629     }
630
631   } @cust_fields;
632 }
633
634 =item cust_colors
635
636 Returns an array of subroutine references (or empty strings) for returning
637 customer information colors.
638
639 As with L<the cust_header subroutine|/cust_header>, the fields returned are
640 defined by the supplied customer fields setting, or if no customer fields
641 setting is supplied, the <B>cust-fields</B> configuration value. 
642
643 =cut
644
645 sub cust_colors {
646   map { 
647     my $method = $_;
648     if ( $method ) {
649       sub { shift->$method(@_) };
650     } else {
651       '';
652     }
653   } @cust_colors;
654 }
655
656 =item cust_styles
657
658 Returns an array of customer information styles.
659
660 As with L<the cust_header subroutine|/cust_header>, the fields returned are
661 defined by the supplied customer fields setting, or if no customer fields
662 setting is supplied, the <B>cust-fields</B> configuration value. 
663
664 =cut
665
666 sub cust_styles {
667   map { 
668     if ( $_ ) {
669       $_;
670     } else {
671       '';
672     }
673   } @cust_styles;
674 }
675
676 =item cust_aligns
677
678 Returns an array or scalar (depending on context) of customer information
679 alignments.
680
681 As with L<the cust_header subroutine|/cust_header>, the fields returned are
682 defined by the supplied customer fields setting, or if no customer fields
683 setting is supplied, the <B>cust-fields</B> configuration value. 
684
685 =cut
686
687 sub cust_aligns {
688   if ( wantarray ) {
689     @cust_aligns;
690   } else {
691     join('', @cust_aligns);
692   }
693 }
694
695 =item cust_links
696
697 Returns an array of links to view/cust_main.cgi, for use with cust_fields.
698
699 =cut
700
701 sub cust_links {
702   my $link = [ FS::CGI::rooturl().'view/cust_main.cgi?', 'custnum' ];
703
704   return map { $_ eq 'cust_status_label' ? '' : $link }
705     @cust_fields;
706 }
707
708 =item is_mobile
709
710 Utility function to determine if the client is a mobile browser.
711
712 =cut
713
714 sub is_mobile {
715   my $ua = $ENV{'HTTP_USER_AGENT'} || '';
716   if ( $ua =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS|S60|Opera Mini|Opera Mobi)/io ) {
717     return 1;
718   }
719   return 0;
720 }
721
722 =item random_id [ DIGITS ]
723
724 Returns a random number of length DIGITS, or if unspecified, a long random 
725 identifier consisting of the timestamp, process ID, and a random number.
726 Anything in the UI that needs a random identifier should use this.
727
728 =cut
729
730 sub random_id {
731   my $digits = shift;
732   if (!defined $NO_RANDOM_IDS) {
733     my $conf = FS::Conf->new;
734     $NO_RANDOM_IDS = $conf->exists('no_random_ids') ? 1 : 0;
735     warn "TEST MODE--RANDOM ID NUMBERS DISABLED\n" if $NO_RANDOM_IDS;
736   }
737   if ( $NO_RANDOM_IDS ) {
738     if ( $digits > 0 ) {
739       return 0;
740     } else {
741       return '0000000000-0000-000000000.000000';
742     }
743   } else {
744     if ($digits > 0) {
745       return int(rand(10 ** $digits));
746     } else {
747       return time . "-$$-" . rand() * 2**32;
748     }
749   }
750 }
751
752 =back
753
754 =cut
755
756 ###
757 # begin JSRPC code...
758 ###
759
760 package FS::UI::Web::JSRPC;
761
762 use strict;
763 use vars qw($DEBUG);
764 use Carp;
765 use Storable qw(nfreeze);
766 use MIME::Base64;
767 use Cpanel::JSON::XS;
768 use FS::CurrentUser;
769 use FS::Record qw(qsearchs);
770 use FS::queue;
771 use FS::CGI qw(rooturl);
772 use FS::Report::Queued::FutureAutobill;
773
774 $DEBUG = 0;
775
776 sub new {
777         my $class = shift;
778         my $self  = {
779                 env => {},
780                 job => shift,
781                 cgi => shift,
782         };
783
784         bless $self, $class;
785
786         croak "CGI object required as second argument" unless $self->{'cgi'};
787
788         return $self;
789 }
790
791 sub process {
792
793   my $self = shift;
794
795   my $cgi = $self->{'cgi'};
796
797   # XXX this should parse JSON foo and build a proper data structure
798   my @args = $cgi->param('arg');
799
800   #work around konqueror bug!
801   @args = map { s/\x00$//; $_; } @args;
802
803   my $sub = $cgi->param('sub'); #????
804
805   warn "FS::UI::Web::JSRPC::process:\n".
806        "  cgi=$cgi\n".
807        "  sub=$sub\n".
808        "  args=".join(', ',@args)."\n"
809     if $DEBUG;
810
811   if ( $sub eq 'start_job' ) {
812
813     $self->start_job(@args);
814
815   } elsif ( $sub eq 'job_status' ) {
816
817     $self->job_status(@args);
818
819   } else {
820
821     die "unknown sub $sub";
822
823   }
824
825 }
826
827 sub start_job {
828   my $self = shift;
829
830   warn "FS::UI::Web::start_job: ". join(', ', @_) if $DEBUG;
831 #  my %param = @_;
832   my %param = ();
833   while ( @_ ) {
834     my( $field, $value ) = splice(@_, 0, 2);
835     unless ( exists( $param{$field} ) ) {
836       $param{$field} = $value;
837     } elsif ( ! ref($param{$field}) ) {
838       $param{$field} = [ $param{$field}, $value ];
839     } else {
840       push @{$param{$field}}, $value;
841     }
842   }
843   $param{CurrentUser} = $FS::CurrentUser::CurrentUser->username;
844   $param{RootURL} = rooturl($self->{cgi}->self_url);
845   warn "FS::UI::Web::start_job\n".
846        join('', map {
847                       if ( ref($param{$_}) ) {
848                         "  $_ => [ ". join(', ', @{$param{$_}}). " ]\n";
849                       } else {
850                         "  $_ => $param{$_}\n";
851                       }
852                     } keys %param )
853     if $DEBUG;
854
855   #first get the CGI params shipped off to a job ASAP so an id can be returned
856   #to the caller
857   
858   my $job = new FS::queue { 'job' => $self->{'job'} };
859   
860   #too slow to insert all the cgi params as individual args..,?
861   #my $error = $queue->insert('_JOB', $cgi->Vars);
862   
863   #rely on FS::queue smartness to freeze/encode the param hash
864
865   my $error = $job->insert( '_JOB', \%param );
866
867   if ( $error ) {
868
869     warn "job not inserted: $error\n"
870       if $DEBUG;
871
872     $error;  #this doesn't seem to be handled well,
873              # will trigger "illegal jobnum" below?
874              # (should never be an error inserting the job, though, only thing
875              #  would be Pg f%*kage)
876   } else {
877
878     warn "job inserted successfully with jobnum ". $job->jobnum. "\n"
879       if $DEBUG;
880
881     $job->jobnum;
882   }
883   
884 }
885
886 sub job_status {
887   my( $self, $jobnum ) = @_; #$url ???
888
889   sleep 1; # XXX could use something better...
890
891   my $job;
892   if ( $jobnum =~ /^(\d+)$/ ) {
893     $job = qsearchs('queue', { 'jobnum' => $jobnum } );
894   } else {
895     die "FS::UI::Web::job_status: illegal jobnum $jobnum\n";
896   }
897
898   my @return;
899   if ( $job && $job->status ne 'failed' && $job->status ne 'done' ) {
900     my ($progress, $action) = split ',', $job->statustext, 2; 
901     $action ||= 'Server processing job';
902     @return = ( 'progress', $progress, $action );
903   } elsif ( !$job ) { #handle job gone case : job successful
904                       # so close popup, redirect parent window...
905     @return = ( 'complete' );
906   } elsif ( $job->status eq 'done' ) {
907     @return = ( 'done', $job->statustext, '' );
908   } else {
909     @return = ( 'error', $job ? $job->statustext : $jobnum );
910   }
911
912   encode_json \@return;
913
914 }
915
916 1;
917