1 package FS::access_user;
2 use base qw( FS::m2m_Common FS::option_Common );
5 use vars qw( $DEBUG $me );
9 use FS::Record qw( qsearch qsearchs dbh );
15 $me = '[FS::access_user]';
19 FS::access_user - Object methods for access_user records
25 $record = new FS::access_user \%hash;
26 $record = new FS::access_user { 'column' => 'value' };
28 $error = $record->insert;
30 $error = $new_record->replace($old_record);
32 $error = $record->delete;
34 $error = $record->check;
38 An FS::access_user object represents an internal access user. FS::access_user
39 inherits from FS::Record. The following fields are currently supported:
51 =item _password_encoding
65 Master customer for this employee (for commissions)
69 Default sales person for this employee (for reports)
83 Creates a new internal access user. To add the user to the database, see L<"insert">.
85 Note that this stores the hash reference, not a distinct copy of the hash it
86 points to. You can ask the object for a copy with the I<hash> method.
90 # the new method can be inherited from FS::Record, if a table method is defined
92 sub table { 'access_user'; }
94 sub _option_table { 'access_user_pref'; }
95 sub _option_namecol { 'prefname'; }
96 sub _option_valuecol { 'prefvalue'; }
100 Adds this record to the database. If there is an error, returns the error,
101 otherwise returns false.
108 my $error = $self->check;
109 return $error if $error;
111 local $SIG{HUP} = 'IGNORE';
112 local $SIG{INT} = 'IGNORE';
113 local $SIG{QUIT} = 'IGNORE';
114 local $SIG{TERM} = 'IGNORE';
115 local $SIG{TSTP} = 'IGNORE';
116 local $SIG{PIPE} = 'IGNORE';
118 my $oldAutoCommit = $FS::UID::AutoCommit;
119 local $FS::UID::AutoCommit = 0;
123 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
127 $error = $self->SUPER::insert(@_);
130 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
133 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
141 Delete this record from the database.
148 local $SIG{HUP} = 'IGNORE';
149 local $SIG{INT} = 'IGNORE';
150 local $SIG{QUIT} = 'IGNORE';
151 local $SIG{TERM} = 'IGNORE';
152 local $SIG{TSTP} = 'IGNORE';
153 local $SIG{PIPE} = 'IGNORE';
155 my $oldAutoCommit = $FS::UID::AutoCommit;
156 local $FS::UID::AutoCommit = 0;
159 my $error = $self->SUPER::delete(@_);
162 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
165 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
171 =item replace OLD_RECORD
173 Replaces the OLD_RECORD with this one in the database. If there is an error,
174 returns the error, otherwise returns false.
181 my $old = ( ref($_[0]) eq ref($new) )
185 local $SIG{HUP} = 'IGNORE';
186 local $SIG{INT} = 'IGNORE';
187 local $SIG{QUIT} = 'IGNORE';
188 local $SIG{TERM} = 'IGNORE';
189 local $SIG{TSTP} = 'IGNORE';
190 local $SIG{PIPE} = 'IGNORE';
192 my $oldAutoCommit = $FS::UID::AutoCommit;
193 local $FS::UID::AutoCommit = 0;
196 return "Must change password when enabling this account"
197 if $old->disabled && !$new->disabled
198 && ( $new->_password =~ /changeme/i
199 || $new->_password eq 'notyet'
202 my $error = $new->SUPER::replace($old, @_);
205 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
208 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
216 Checks all fields to make sure this is a valid internal access user. If there is
217 an error, returns the error, otherwise returns false. Called by the insert
222 # the check method should currently be supplied - FS::Record contains some
223 # data checking routines
229 $self->ut_numbern('usernum')
230 || $self->ut_alpha_lower('username')
231 || $self->ut_textn('_password')
232 || $self->ut_textn('last')
233 || $self->ut_textn('first')
234 || $self->ut_foreign_keyn('user_custnum', 'cust_main', 'custnum')
235 || $self->ut_foreign_keyn('report_salesnum', 'sales', 'salesnum')
236 || $self->ut_enum('disabled', [ '', 'Y' ] )
238 return $error if $error;
245 Returns a name string for this user: "Last, First".
251 return $self->username
252 if $self->get('last') eq 'Lastname' && $self->first eq 'Firstname'
253 or $self->get('last') eq '' && $self->first eq '';
254 return $self->get('last'). ', '. $self->first;
259 Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
266 qsearchs( 'cust_main', { 'custnum' => $self->user_custnum } );
271 Returns the FS::sales object (see L<FS::sales>), if any, for this
278 qsearchs( 'sales', { 'salesnum' => $self->report_salesnum } );
281 =item access_usergroup
283 Returns links to the the groups this user is a part of, as FS::access_usergroup
284 objects (see L<FS::access_usergroup>).
288 Returns the number of agents this user can view (via group membership).
295 'SELECT COUNT(DISTINCT agentnum) FROM access_usergroup
296 JOIN access_groupagent USING ( groupnum )
304 Returns a list of agentnums this user can view (via group membership).
310 my $sth = dbh->prepare(
311 "SELECT DISTINCT agentnum FROM access_usergroup
312 JOIN access_groupagent USING ( groupnum )
314 ) or die dbh->errstr;
315 $sth->execute($self->usernum) or die $sth->errstr;
316 map { $_->[0] } @{ $sth->fetchall_arrayref };
321 Returns a hashref of agentnums this user can view.
327 scalar( { map { $_ => 1 } $self->agentnums } );
330 =item agentnums_sql [ HASHREF | OPTION => VALUE ... ]
332 Returns an sql fragement to select only agentnums this user can view.
334 Options are passed as a hashref or a list. Available options are:
340 The frament will also allow the selection of null agentnums.
344 The fragment will also allow the selection of null agentnums if the current
345 user has the provided access right
349 Optional table name in which agentnum is being checked. Sometimes required to
350 resolve 'column reference "agentnum" is ambiguous' errors.
354 All agents will be viewable if the current user has the provided access right.
355 Defaults to 'View customers of all agents'.
363 my %opt = ref($_[0]) ? %{$_[0]} : @_;
365 my $agentnum = $opt{'table'} ? $opt{'table'}.'.agentnum' : 'agentnum';
369 my $viewall_right = $opt{'viewall_right'} || 'View customers of all agents';
370 if ( $self->access_right($viewall_right) ) {
371 push @or, "$agentnum IS NOT NULL";
373 my @agentnums = $self->agentnums;
374 push @or, "$agentnum IN (". join(',', @agentnums). ')'
378 push @or, "$agentnum IS NULL"
380 || ( $opt{'null_right'} && $self->access_right($opt{'null_right'}) );
382 return ' 1 = 0 ' unless scalar(@or);
383 '( '. join( ' OR ', @or ). ' )';
389 Returns true if the user can view the specified agent.
391 Also accepts optional hashref cache, to avoid redundant database calls.
396 my( $self, $agentnum, $cache ) = @_;
398 return $cache->{$self->usernum}->{$agentnum}
399 if $cache->{$self->usernum}->{$agentnum};
400 my $sth = dbh->prepare(
401 "SELECT COUNT(*) FROM access_usergroup
402 JOIN access_groupagent USING ( groupnum )
403 WHERE usernum = ? AND agentnum = ?"
404 ) or die dbh->errstr;
405 $sth->execute($self->usernum, $agentnum) or die $sth->errstr;
406 $cache->{$self->usernum}->{$agentnum} = $sth->fetchrow_arrayref->[0];
408 return $cache->{$self->usernum}->{$agentnum};
411 =item agents [ HASHREF | OPTION => VALUE ... ]
413 Returns the list of agents this user can view (via group membership), as
414 FS::agent objects. Accepts the same options as the agentnums_sql method.
422 'hashref' => { disabled=>'' },
423 'extra_sql' => ' AND '. $self->agentnums_sql(@_),
424 'order_by' => 'ORDER BY agent',
428 =item access_users [ HASHREF | OPTION => VALUE ... ]
430 Returns an array of FS::access_user objects, one for each non-disabled
431 access_user in the system that shares an agent (via group membership) with
432 the invoking object. Regardless of options and agents, will always at
433 least return the invoking user and any users who have viewall_right.
435 Accepts the following options:
441 Only return users who appear in the usernum field of this table
445 Include disabled users if true (defaults to false)
449 All users will be returned if the current user has the provided
450 access right, regardless of agents (other filters still apply.)
451 Defaults to 'View customers of all agents'
455 #Leaving undocumented until such time as this functionality is actually used
459 #Users with no agents will be returned.
463 #Users with no agents will be returned if the current user has the provided
468 my %opt = ref($_[0]) ? %{$_[0]} : @_;
469 my $table = $opt{'table'};
470 my $search = { 'table' => 'access_user' };
471 $search->{'hashref'} = $opt{'disabled'} ? {} : { 'disabled' => '' };
472 $search->{'addl_from'} = "INNER JOIN $table ON (access_user.usernum = $table.usernum)"
474 my @access_users = qsearch($search);
475 my $viewall_right = $opt{'viewall_right'} || 'View customers of all agents';
476 return @access_users if $self->access_right($viewall_right);
477 #filter for users with agents $self can view
479 my $agentnum_cache = {};
481 foreach my $access_user (@access_users) {
482 # you can always view yourself, regardless of agents,
483 # and you can always view someone who can view you,
484 # since they might have affected your customers
485 if ( ($self->usernum eq $access_user->usernum)
486 || $access_user->access_right($viewall_right)
488 push(@out,$access_user);
491 # if user has no agents, you need null or null_right to view
492 my @agents = $access_user->agents('viewall_right'=>'NONE'); #handled viewall_right above
495 ( $opt{'null_right'} && $self->access_right($opt{'null_right'}) )
497 push(@out,$access_user);
501 # otherwise, you need an agent in common
502 foreach my $agent (@agents) {
503 if ($self->agentnum($agent->agentnum,$agentnum_cache)) {
504 push(@out,$access_user);
512 =item access_users_hashref [ HASHREF | OPTION => VALUE ... ]
514 Accepts same options as L</access_users>. Returns a hashref of
515 users, with keys of usernum and values of username.
519 sub access_users_hashref {
521 my %access_users = map { $_->usernum => $_->username }
522 $self->access_users(@_);
523 return \%access_users;
526 =item access_right RIGHTNAME | LISTREF
528 Given a right name or a list reference of right names, returns true if this
529 user has this right, or, for a list, one of the rights (currently via group
530 membership, eventually also via user overrides).
535 my( $self, $rightname ) = @_;
537 $rightname = [ $rightname ] unless ref($rightname);
539 warn "$me access_right called on ". join(', ', @$rightname). "\n"
542 #some caching of ACL requests for low-hanging fruit perf improvement
543 #since we get a new $CurrentUser object each page view there shouldn't be any
544 #issues with stickiness
545 if ( $self->{_ACLcache} ) {
547 unless ( grep !exists($self->{_ACLcache}{$_}), @$rightname ) {
548 warn "$me ACL cache hit for ". join(', ', @$rightname). "\n"
550 return scalar( grep $self->{_ACLcache}{$_}, @$rightname );
553 warn "$me ACL cache miss for ". join(', ', @$rightname). "\n"
558 warn "initializing ACL cache\n"
560 $self->{_ACLcache} = {};
564 my $has_right = ' rightname IN ('. join(',', map '?', @$rightname ). ') ';
566 my $sth = dbh->prepare("
567 SELECT groupnum FROM access_usergroup
568 LEFT JOIN access_group USING ( groupnum )
569 LEFT JOIN access_right
570 ON ( access_group.groupnum = access_right.rightobjnum )
572 AND righttype = 'FS::access_group'
575 ") or die dbh->errstr;
576 $sth->execute($self->usernum, @$rightname) or die $sth->errstr;
577 my $row = $sth->fetchrow_arrayref;
579 my $return = $row ? $row->[0] : '';
581 #just caching the single-rightname hits should be enough of a win for now
582 if ( scalar(@$rightname) == 1 ) {
583 $self->{_ACLcache}{${$rightname}[0]} = $return;
590 =item default_customer_view
592 Returns the default customer view for this user, from the
593 "default_customer_view" user preference, the "cust_main-default_view" config,
594 or the hardcoded default, "basics" (formerly "jumbo" prior to 3.0).
598 sub default_customer_view {
601 $self->option('default_customer_view')
602 || FS::Conf->new->config('cust_main-default_view')
603 || 'basics'; #s/jumbo/basics/ starting with 3.0
607 =item spreadsheet_format [ OVERRIDE ]
609 Returns a hashref of this user's Excel spreadsheet download settings:
610 'extension' (xls or xlsx), 'class' (Spreadsheet::WriteExcel or
611 Excel::Writer::XLSX), and 'mime_type'. If OVERRIDE is 'XLS' or 'XLSX',
612 use that instead of the user's setting.
616 # is there a better place to put this?
620 class => 'Spreadsheet::WriteExcel',
621 mime_type => 'application/vnd.ms-excel',
624 extension => '.xlsx',
625 class => 'Excel::Writer::XLSX',
626 mime_type => # it's on wikipedia, it must be true
627 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
631 sub spreadsheet_format {
633 my $override = shift;
636 || $self->option('spreadsheet_format')
637 || FS::Conf->new->config('spreadsheet_format')
645 Returns true if this user has the name of a known system account. These
646 users cannot log into the web interface and can't have passwords set.
652 return grep { $_ eq $self->username } ( qw(
663 =item change_password NEW_PASSWORD
667 sub change_password {
668 #my( $self, $password ) = @_;
669 #FS::Auth->auth_class->change_password( $self, $password );
670 FS::Auth->auth_class->change_password( @_ );
673 =item change_password_fields NEW_PASSWORD
677 sub change_password_fields {
678 #my( $self, $password ) = @_;
679 #FS::Auth->auth_class->change_password_fields( $self, $password );
680 FS::Auth->auth_class->change_password_fields( @_ );
689 L<FS::Record>, schema.html from the base documentation.