1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
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
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.
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.
30 # CONTRIBUTION SUBMISSION POLICY:
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.)
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.
47 # END BPS TAGGED BLOCK }}}
57 # Import configuration data from the lexcial scope of __PACKAGE__ (or
58 # at least where those two Subroutines are defined.)
60 our (%FIELD_METADATA, %dispatch, %can_bundle);
62 # Lower Case version of FIELDS, for case insensitivity
63 my %lcfields = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
68 # Private Member Variables (which should get cleaned)
69 $self->{'_sql_transalias'} = undef;
70 $self->{'_sql_trattachalias'} = undef;
71 $self->{'_sql_cf_alias'} = undef;
72 $self->{'_sql_object_cfv_alias'} = undef;
73 $self->{'_sql_watcher_join_users_alias'} = undef;
74 $self->{'_sql_query'} = '';
75 $self->{'_sql_looking_at'} = {};
81 if ($args{'FIELD'} eq 'EffectiveId' &&
82 (!$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) ) {
83 $self->{'looking_at_effective_id'} = 1;
86 if ($args{'FIELD'} eq 'Type' &&
87 (!$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) ) {
88 $self->{'looking_at_type'} = 1;
91 # All SQL stuff goes into one SB subclause so we can deal with all
93 $self->SUPER::Limit(%args,
94 SUBCLAUSE => 'ticketsql');
98 # All SQL stuff goes into one SB subclause so we can deal with all
102 $this->SUPER::Join(@_,
103 SUBCLAUSE => 'ticketsql');
108 $_[0]->SUPER::_OpenParen( 'ticketsql' );
111 $_[0]->SUPER::_CloseParen( 'ticketsql' );
118 =head2 Robert's Simple SQL Parser
120 Documentation In Progress
122 The Parser/Tokenizer is a relatively simple state machine that scans through a SQL WHERE clause type string extracting a token at a time (where a token is:
124 VALUE -> quoted string or number
125 AGGREGator -> AND or OR
126 KEYWORD -> quoted string or single word
127 OPerator -> =,!=,LIKE,etc..
128 PARENthesis -> open or close.
130 And that stream of tokens is passed through the "machine" in order to build up a structure that looks like:
136 That also deals with parenthesis for nesting. (The parentheses are
137 just handed off the SearchBuilder)
142 my ($self, @bundle) = @_;
143 return unless @bundle;
145 if ( @bundle == 1 ) {
146 $bundle[0]->{'dispatch'}->(
152 ENTRYAGGREGATOR => $bundle[0]->{ea},
153 SUBKEY => $bundle[0]->{subkey},
158 foreach my $chunk (@bundle) {
164 ENTRYAGGREGATOR => $chunk->{ea},
165 SUBKEY => $chunk->{subkey},
168 $bundle[0]->{dispatch}->( $self, \@args );
173 my ($self,$string) = @_;
178 $callback{'OpenParen'} = sub {
179 $self->_close_bundle(@bundle); @bundle = ();
182 $callback{'CloseParen'} = sub {
183 $self->_close_bundle(@bundle); @bundle = ();
186 $callback{'EntryAggregator'} = sub { $ea = $_[0] || '' };
187 $callback{'Condition'} = sub {
188 my ($key, $op, $value) = @_;
190 # key has dot then it's compound variant and we have subkey
192 ($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/;
194 # normalize key and get class (type)
196 if (exists $lcfields{lc $key}) {
197 $key = $lcfields{lc $key};
198 $class = $FIELD_METADATA{$key}->[0];
200 die "Unknown field '$key' in '$string'" unless $class;
202 # replace __CurrentUser__ with id
203 $value = $self->CurrentUser->id if $value eq '__CurrentUser__';
206 unless( $dispatch{ $class } ) {
207 die "No dispatch method for class '$class'"
209 my $sub = $dispatch{ $class };
211 if ( $can_bundle{ $class }
213 || ( $bundle[-1]->{dispatch} == $sub
214 && $bundle[-1]->{key} eq $key
215 && $bundle[-1]->{subkey} eq $subkey
230 $self->_close_bundle(@bundle); @bundle = ();
231 $sub->( $self, $key, $op, $value,
232 SUBCLAUSE => '', # don't need anymore
233 ENTRYAGGREGATOR => $ea,
237 $self->{_sql_looking_at}{lc $key} = 1;
240 RT::SQL::Parse($string, \%callback);
241 $self->_close_bundle(@bundle); @bundle = ();
253 for my $f (keys %{$clauses}) {
257 # Build SQL from the data hash
258 for my $data ( @{ $clauses->{$f} } ) {
259 $sql .= $data->[0] unless $first; $first=0; # ENTRYAGGREGATOR
260 $sql .= " '". $data->[2] . "' "; # FIELD
261 $sql .= $data->[3] . " "; # OPERATOR
262 $sql .= "'". $data->[4] . "' "; # VALUE
265 push @sql, " ( " . $sql . " ) ";
268 return join("AND",@sql);
273 Convert a RT-SQL string into a set of SearchBuilder restrictions.
275 Returns (1, 'Status message') on success and (0, 'Error Message') on
284 my ($self,$query) = @_;
287 # preserve first_row and show_rows across the CleanSlate
288 local ($self->{'first_row'}, $self->{'show_rows'});
293 return (1, $self->loc("No Query")) unless $query;
295 $self->{_sql_query} = $query;
296 eval { $self->_parser( $query ); };
298 $RT::Logger->error( $@ );
302 # We only want to look at EffectiveId's (mostly) for these searches.
303 unless ( exists $self->{_sql_looking_at}{'effectiveid'} ) {
304 #TODO, we shouldn't be hard #coding the tablename to main.
305 $self->SUPER::Limit( FIELD => 'EffectiveId',
307 ENTRYAGGREGATOR => 'AND',
311 # FIXME: Need to bring this logic back in
313 # if ($self->_isLimited && (! $self->{'looking_at_effective_id'})) {
314 # $self->SUPER::Limit( FIELD => 'EffectiveId',
317 # VALUE => 'main.id'); #TODO, we shouldn't be hard coding the tablename to main.
319 # --- This is hardcoded above. This comment block can probably go.
320 # Or, we need to reimplement the looking_at_effective_id toggle.
322 # Unless we've explicitly asked to look at a specific Type, we need
324 unless ( $self->{looking_at_type} ) {
325 $self->SUPER::Limit( FIELD => 'Type', VALUE => 'ticket' );
328 # We don't want deleted tickets unless 'allow_deleted_search' is set
329 unless( $self->{'allow_deleted_search'} ) {
330 $self->SUPER::Limit( FIELD => 'Status',
336 # set SB's dirty flag
337 $self->{'must_redo_search'} = 1;
338 $self->{'RecalcTicketLimits'} = 0;
340 return (1, $self->loc("Valid Query"));
345 Returns the query that this object was initialized with
350 return ($_[0]->{_sql_query});
355 '=' => '!=', '!=' => '=', '<>' => '=',
356 '>' => '<=', '<' => '>=', '>=' => '<', '<=' => '>',
357 'is' => 'IS NOT', 'is not' => 'IS',
358 'like' => 'NOT LIKE', 'not like' => 'LIKE',
359 'matches' => 'NOT MATCHES', 'not matches' => 'MATCHES',
360 'startswith' => 'NOT STARTSWITH', 'not startswith' => 'STARTSWITH',
361 'endswith' => 'NOT ENDSWITH', 'not endswith' => 'ENDSWITH',
364 my %range = map { $_ => 1 } qw(> >= < <=);
366 sub ClassifySQLOperation {
371 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
376 if ( 'is not' eq lc($op) || 'is' eq lc($op) ) {
380 return ($is_negative, $is_null, $inv{lc $op}, $range{lc $op});
389 Most of the RT code does not use Exceptions (die/eval) but it is used
390 in the TicketSQL code for simplicity and historical reasons. Lest you
391 be worried that the dies will trigger user visible errors, all are
394 99% of the dies fall in subroutines called via FromSQL and then parse.
395 (This includes all of the _FooLimit routines in Tickets_Overlay.pm.)
396 The other 1% or so are via _ProcessRestrictions.
398 All dies are trapped by eval {}s, and will be logged at the 'error'
399 log level. The general failure mode is to not display any tickets.
405 Legacy LimitFoo routines build up a RestrictionsHash
407 _ProcessRestrictions converts the Restrictions to Clauses
410 Clauses are converted to RT-SQL (TicketSQL)
414 FromSQL calls the parser
416 The parser calls the _FooLimit routines to do DBIx::SearchBuilder
419 And then the normal SearchBuilder/Ticket routines are used for