1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
5# This software is Copyright (c) 1996-2021 Best Practical Solutions, LLC
6#                                          <sales@bestpractical.com>
7#
8# (Except where explicitly superseded by other copyright notices)
9#
10#
11# LICENSE:
12#
13# This work is made available to you under the terms of Version 2 of
14# the GNU General Public License. A copy of that license should have
15# been provided with this software, but in any event can be snarfed
16# from www.gnu.org.
17#
18# This work is distributed in the hope that it will be useful, but
19# WITHOUT ANY WARRANTY; without even the implied warranty of
20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21# General Public License for more details.
22#
23# You should have received a copy of the GNU General Public License
24# along with this program; if not, write to the Free Software
25# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26# 02110-1301 or visit their web page on the internet at
27# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28#
29#
30# CONTRIBUTION SUBMISSION POLICY:
31#
32# (The following paragraph is not intended to limit the rights granted
33# to you to modify and distribute this software under the terms of
34# the GNU General Public License and is only of importance to you if
35# you choose to contribute your changes and enhancements to the
36# community by submitting them to Best Practical Solutions, LLC.)
37#
38# By intentionally submitting any modifications, corrections or
39# derivatives to this work, or any other work intended for use with
40# Request Tracker, to Best Practical Solutions, LLC, you confirm that
41# you are the copyright holder for those contributions and you grant
42# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43# royalty-free, perpetual, license to use, copy, create derivative
44# works based on those contributions, and sublicense and distribute
45# those contributions and any derivatives thereof.
46#
47# END BPS TAGGED BLOCK }}}
48
49package RT::CustomFieldValues::External;
50
51use strict;
52use warnings;
53
54use base qw(RT::CustomFieldValues);
55
56=head1 NAME
57
58RT::CustomFieldValues::External - Pull possible values for a custom
59field from an arbitrary external data source.
60
61=head1 SYNOPSIS
62
63Custom field value lists can be produced by creating a class that
64inherits from C<RT::CustomFieldValues::External>, and overloading
65C<SourceDescription> and C<ExternalValues>.  See
66L<RT::CustomFieldValues::Groups> for a simple example.
67
68=head1 DESCRIPTION
69
70Subclasses should implement the following methods:
71
72=head2 SourceDescription
73
74This method should return a string describing the data source; this is
75the identifier by which the user will see the dropdown.
76
77=head2 ExternalValues
78
79This method should return an array reference of hash references.  The
80hash references must contain a key for C<name> and can optionally contain
81keys for C<description>, C<sortorder>, and C<category>. If supplying a
82category, you must also set the category the custom field is based on in
83the custom field configuration page.
84
85=head1 SEE ALSO
86
87F<docs/extending/external_custom_fields.pod>
88
89=cut
90
91sub _Init {
92    my $self = shift;
93    $self->Table( '' );
94    return ( $self->SUPER::_Init(@_) );
95}
96
97sub CleanSlate {
98    my $self = shift;
99    delete $self->{ $_ } foreach qw(
100        __external_cf
101        __external_cf_limits
102    );
103    return $self->SUPER::CleanSlate(@_);
104}
105
106sub _ClonedAttributes {
107    my $self = shift;
108    return qw(
109        __external_cf
110        __external_cf_limits
111    ), $self->SUPER::_ClonedAttributes;
112}
113
114sub Limit {
115    my $self = shift;
116    my %args = (@_);
117    push @{ $self->{'__external_cf_limits'} ||= [] }, {
118        %args,
119        CALLBACK => $self->__BuildLimitCheck( %args ),
120    };
121    return $self->SUPER::Limit( %args );
122}
123
124sub __BuildLimitCheck {
125    my ($self, %args) = (@_);
126    return undef unless $args{'FIELD'} =~ /^(?:Name|Description)$/;
127
128    my $condition = $args{VALUE};
129    my $op = $args{'OPERATOR'} || '=';
130    my $field = $args{FIELD};
131
132    return sub {
133        my $record = shift;
134        my $value = $record->$field;
135        return 0 unless defined $value;
136        if ($op eq "=") {
137            return 0 unless $value eq $condition;
138        } elsif ($op eq "!=" or $op eq "<>") {
139            return 0 unless $value ne $condition;
140        } elsif (uc($op) eq "LIKE") {
141            return 0 unless $value =~ /\Q$condition\E/i;
142        } elsif (uc($op) eq "NOT LIKE") {
143            return 0 unless $value !~ /\Q$condition\E/i;
144        } else {
145            return 0;
146        }
147        return 1;
148    };
149}
150
151sub __BuildAggregatorsCheck {
152    my $self = shift;
153    my @cbs = grep {$_->{CALLBACK}} @{ $self->{'__external_cf_limits'} };
154    return undef unless @cbs;
155
156    my %h = (
157        OR  => sub { defined $_[0] ? ($_[0] || $_[1]) : $_[1] },
158        AND => sub { defined $_[0] ? ($_[0] && $_[1]) : $_[1] },
159    );
160
161    return sub {
162        my ($sb, $record) = @_;
163        my $ok;
164        for my $limit ( @cbs ) {
165            $ok = $h{$limit->{ENTRYAGGREGATOR} || 'OR'}->(
166                $ok, $limit->{CALLBACK}->($record),
167            );
168        }
169        return $ok;
170    };
171}
172
173sub _DoSearch {
174    my $self = shift;
175
176    delete $self->{'items'};
177
178    my %defaults = (
179            id => 1,
180            name => '',
181            customfield => $self->{'__external_cf'},
182            sortorder => 0,
183            description => '',
184            category => undef,
185            creator => RT->SystemUser->id,
186            created => undef,
187            lastupdatedby => RT->SystemUser->id,
188            lastupdated => undef,
189    );
190
191    my $i = 0;
192
193    my $check = $self->__BuildAggregatorsCheck;
194
195    foreach( $self->_SortValues( @{ $self->ExternalValues } ) ) {
196        my $value = $self->NewItem;
197        $value->LoadFromHash( { %defaults, %$_ } );
198        next if $check && !$check->( $self, $value );
199        $self->AddRecord( $value );
200        last if $self->RowsPerPage and ++$i >= $self->RowsPerPage;
201    }
202    $self->{'must_redo_search'} = 0;
203    return $self->_RecordCount;
204}
205
206sub _SortValues {
207    my $self  = shift;
208    my @items = @_;
209
210    no warnings 'uninitialized';
211
212    return sort { $a->{sortorder} <=> $b->{sortorder} || lc($a->{name}) cmp lc($b->{name}) }
213               @items;
214}
215
216sub _DoCount {
217    my $self = shift;
218
219    my $count;
220    $count = $self->_DoSearch if $self->{'must_redo_search'};
221    $count = $self->_RecordCount unless defined $count;
222
223    return $self->{'count_all'} = $self->{'raw_rows'} = $count;
224}
225
226sub LimitToCustomField {
227    my $self = shift;
228    $self->{'__external_cf'} = $_[0];
229    return $self->SUPER::LimitToCustomField( @_ );
230}
231
232sub _SingularClass {
233    "RT::CustomFieldValue"
234}
235
236RT::Base->_ImportOverlays();
237
2381;
239