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
49=head1 NAME
50
51RT::System
52
53=head1 DESCRIPTION
54
55RT::System is a simple global object used as a focal point for things
56that are system-wide.
57
58It works sort of like an RT::Record, except it's really a single object that has
59an id of "1" when instantiated.
60
61This gets used by the ACL system so that you can have rights for the scope "RT::System"
62
63In the future, there will probably be other API goodness encapsulated here.
64
65=cut
66
67
68package RT::System;
69
70use strict;
71use warnings;
72
73use base qw/RT::Record/;
74
75use Role::Basic 'with';
76with "RT::Record::Role::Roles",
77     "RT::Record::Role::Rights" => { -excludes => [qw/AvailableRights RightCategories/] };
78
79use RT::ACL;
80use RT::ACE;
81use Data::GUID;
82
83__PACKAGE__->AddRight( Admin   => SuperUser           => 'Do anything and everything'); # loc
84__PACKAGE__->AddRight( Staff   => ShowUserHistory     => 'Show history of public user properties'); # loc
85__PACKAGE__->AddRight( Admin   => AdminUsers          => 'Create, modify and delete users'); # loc
86__PACKAGE__->AddRight( Admin   => AdminCustomRoles    => 'Create, modify and delete custom roles'); # loc
87__PACKAGE__->AddRight( Staff   => ModifySelf          => "Modify one's own RT account"); # loc
88__PACKAGE__->AddRight( Staff   => ShowArticlesMenu    => 'Show Articles menu'); # loc
89__PACKAGE__->AddRight( Admin   => ShowConfigTab       => 'Show Admin menu'); # loc
90__PACKAGE__->AddRight( Admin   => ShowApprovalsTab    => 'Show Approvals tab'); # loc
91__PACKAGE__->AddRight( Staff   => ShowAssetsMenu      => 'Show Assets menu'); # loc
92__PACKAGE__->AddRight( Staff   => ShowGlobalTemplates => 'Show global templates'); # loc
93__PACKAGE__->AddRight( General => LoadSavedSearch     => 'Allow loading of saved searches'); # loc
94__PACKAGE__->AddRight( General => CreateSavedSearch   => 'Allow creation of saved searches'); # loc
95__PACKAGE__->AddRight( Admin   => ExecuteCode         => 'Allow writing Perl code in templates, scrips, etc'); # loc
96__PACKAGE__->AddRight( General => SeeSelfServiceGroupTicket => 'See tickets for other group members in SelfService' ); # loc
97__PACKAGE__->AddRight( Staff   => ShowSearchAdvanced    => 'Show search "Advanced" menu' ); # loc
98__PACKAGE__->AddRight( Staff   => ShowSearchBulkUpdate  => 'Show search "Bulk Update" menu' ); # loc
99
100=head2 AvailableRights
101
102Returns a hashref of available rights for this object.  The keys are the
103right names and the values are a description of what the rights do.
104
105This method as well returns rights of other RT objects, like
106L<RT::Queue> or L<RT::Group>, to allow users to apply those rights
107globally.
108
109If an L<RT::Principal> is passed as the first argument, the available
110rights will be limited to ones which make sense for the principal.
111Currently only role groups are supported and rights announced by object
112types to which the role group doesn't apply are not returned.
113
114=cut
115
116sub AvailableRights {
117    my $self = shift;
118    my $principal = shift;
119    my $class = ref($self) || $self;
120
121    my @rights;
122    if ($principal and $principal->IsRoleGroup) {
123        my $role = $principal->Object->Name;
124        for my $class (keys %RT::ACE::RIGHTS) {
125            next unless $class->DOES('RT::Record::Role::Roles') and $class->HasRole($role) and $class ne "RT::System";
126            push @rights, values %{ $RT::ACE::RIGHTS{$class} };
127        }
128    } else {
129        @rights = map {values %{$_}} values %RT::ACE::RIGHTS;
130    }
131
132    my %rights;
133    $rights{$_->{Name}} = $_->{Description} for @rights;
134
135    delete $rights{ExecuteCode} if RT->Config->Get('DisallowExecuteCode');
136
137    return \%rights;
138}
139
140=head2 RightCategories
141
142Returns a hashref where the keys are rights for this type of object and the
143values are the category (General, Staff, Admin) the right falls into.
144
145=cut
146
147sub RightCategories {
148    my $self = shift;
149    my $class = ref($self) || $self;
150
151    my %rights;
152    $rights{$_->{Name}} = $_->{Category}
153        for map {values %{$_}} values %RT::ACE::RIGHTS;
154    return \%rights;
155}
156
157sub _Init {
158    my $self = shift;
159    $self->SUPER::_Init (@_) if @_ && $_[0];
160}
161
162=head2 id
163
164Returns RT::System's id. It's 1.
165
166=cut
167
168*Id = \&id;
169sub id { return 1 }
170
171sub UID { return "RT::System" }
172
173=head2 Load
174
175Since this object is pretending to be an RT::Record, we need a load method.
176It does nothing
177
178=cut
179
180sub Load    { return 1 }
181sub Name    { return 'RT System' }
182sub __Set   { return 0 }
183sub __Value { return 0 }
184sub Create  { return 0 }
185sub Delete  { return 0 }
186
187sub SubjectTag {
188    my $self = shift;
189    my $queue = shift;
190
191    return $queue->SubjectTag if $queue;
192
193    my $queues = RT::Queues->new( $self->CurrentUser );
194    $queues->Limit( FIELD => 'SubjectTag', OPERATOR => 'IS NOT', VALUE => 'NULL' );
195    return $queues->DistinctFieldValues('SubjectTag');
196}
197
198=head2 QueueCacheNeedsUpdate ( 1 )
199
200Attribute to decide when SelectQueue needs to flush the list of queues
201and retrieve new ones.  Set when queues are created, enabled/disabled
202and on certain acl changes.  Should also better understand group management.
203
204If passed a true value, will update the attribute to be the current time.
205
206=cut
207
208sub QueueCacheNeedsUpdate {
209    my $self = shift;
210    my $update = shift;
211
212    if ($update) {
213        return $self->SetAttribute(Name => 'QueueCacheNeedsUpdate', Content => time);
214    } else {
215        my $cache = $self->FirstAttribute('QueueCacheNeedsUpdate');
216        return (defined $cache ? $cache->Content : 0 );
217    }
218}
219
220=head2 CustomRoleCacheNeedsUpdate ( 1 )
221
222Attribute to decide when we need to flush the list of custom roles
223and re-register any changes.  Set when roles are created, enabled/disabled, etc.
224
225If passed a true value, will update the attribute to be the current time.
226
227=cut
228
229sub CustomRoleCacheNeedsUpdate {
230    my $self = shift;
231    my $update = shift;
232
233    if ($update) {
234        return $self->SetAttribute(Name => 'CustomRoleCacheNeedsUpdate', Content => time);
235    } else {
236        my $cache = $self->FirstAttribute('CustomRoleCacheNeedsUpdate');
237        return (defined $cache ? $cache->Content : 0 );
238    }
239}
240
241=head2 ConfigCacheNeedsUpdate ( 1 )
242
243Attribute to decide when we need to flush the database settings
244and re-register any changes.  Set when settings are created, enabled/disabled, etc.
245
246If passed a true value, will update the attribute to be the current time.
247
248=cut
249
250sub ConfigCacheNeedsUpdate {
251    my $self = shift;
252    my $time = shift;
253
254    if ($time) {
255        return $self->SetAttribute(Name => 'ConfigCacheNeedsUpdate', Content => $time);
256    } else {
257        my $cache = $self->FirstAttribute('ConfigCacheNeedsUpdate');
258        return (defined $cache ? $cache->Content : 0 );
259    }
260}
261
262# This needs to be in RT::System as RT::Interface::Web and RT::Interface::Email both use this
263my $lifecycle_cache_time = time;
264sub MaybeRebuildLifecycleCache {
265    my $needs_update = RT->System->LifecycleCacheNeedsUpdate;
266    if ( $needs_update > $lifecycle_cache_time ) {
267        RT::Lifecycle->FillCache;
268        $lifecycle_cache_time = $needs_update;
269    }
270}
271
272=head2 LifecycleCacheNeedsUpdate ( 1 )
273
274Attribute to decide when we need to flush the list of lifecycles
275and re-register any changes. This is needed for the lifecycle UI editor.
276
277If passed a true value, will update the attribute to be the current time.
278
279=cut
280
281sub LifecycleCacheNeedsUpdate {
282    my $self   = shift;
283    my $update = shift;
284
285    if ($update) {
286        return $self->SetAttribute(Name => 'LifecycleCacheNeedsUpdate', Content => time);
287    }
288    else {
289        my $cache = $self->FirstAttribute('LifecycleCacheNeedsUpdate');
290        return (defined $cache ? $cache->Content : 0);
291    }
292}
293
294=head2 AddUpgradeHistory package, data
295
296Adds an entry to the upgrade history database. The package can be either C<RT>
297for core RT upgrades, or the fully qualified name of a plugin. The data must be
298a hash reference.
299
300=cut
301
302sub AddUpgradeHistory {
303    my $self  = shift;
304    my $package = shift;
305    my $data  = shift;
306
307    $data->{timestamp}  ||= time;
308    $data->{rt_version} ||= $RT::VERSION;
309
310    my $upgrade_history_attr = $self->FirstAttribute('UpgradeHistory');
311    my $upgrade_history = $upgrade_history_attr ? $upgrade_history_attr->Content : {};
312
313    push @{ $upgrade_history->{$package} }, $data;
314
315    $self->SetAttribute(
316        Name    => 'UpgradeHistory',
317        Content => $upgrade_history,
318    );
319}
320
321=head2 UpgradeHistory [package]
322
323Returns the entries of RT's upgrade history. If a package is specified, the list
324of upgrades for that package will be returned. Otherwise a hash reference of
325C<< package => [upgrades] >> will be returned.
326
327=cut
328
329sub UpgradeHistory {
330    my $self  = shift;
331    my $package = shift;
332
333    my $upgrade_history_attr = $self->FirstAttribute('UpgradeHistory');
334    my $upgrade_history = $upgrade_history_attr ? $upgrade_history_attr->Content : {};
335
336    if ($package) {
337        return @{ $upgrade_history->{$package} || [] };
338    }
339
340    return $upgrade_history;
341}
342
343sub ParsedUpgradeHistory {
344    my $self = shift;
345    my $package = shift;
346
347    my $version_status = "Current version: ";
348    if ( $package eq 'RT' ){
349        $version_status .= $RT::VERSION;
350    } elsif ( grep {/$package/} @{RT->Config->Get('Plugins')} ) {
351        no strict 'refs';
352        $version_status .= ${ $package . '::VERSION' };
353    } else {
354        $version_status = "Not currently loaded";
355    }
356
357    my %ids;
358    my @lines;
359
360    my @events = $self->UpgradeHistory( $package );
361    for my $event (@events) {
362        if ($event->{stage} eq 'before' or (($event->{action}||'') eq 'insert' and not $event->{full_id})) {
363            if (not $event->{full_id}) {
364                # For upgrade done in the 4.1 series without GUIDs
365                if (($event->{type}||'') eq 'full upgrade') {
366                    $event->{full_id} = $event->{individual_id} = Data::GUID->new->as_string;
367                } else {
368                    $event->{individual_id} = Data::GUID->new->as_string;
369                    $event->{full_id} = (@lines ? $lines[-1]{full_id} : Data::GUID->new->as_string);
370                }
371                $event->{return_value} = [1] if $event->{stage} eq 'after';
372            }
373            if ($ids{$event->{full_id}}) {
374                my $kids = $ids{$event->{full_id}}{sub_events} ||= [];
375                # Stitch non-"upgrade"s beneath the previous "upgrade"
376                if ( @{$kids} and $event->{action} ne 'upgrade' and $kids->[-1]{action} eq 'upgrade') {
377                    push @{ $kids->[-1]{sub_events} }, $event;
378                } else {
379                    push @{ $kids }, $event;
380                }
381            } else {
382                push @lines, $event;
383            }
384            $ids{$event->{individual_id}} = $event;
385        } elsif ($event->{stage} eq 'after') {
386            if (not $event->{individual_id}) {
387                if (($event->{type}||'') eq 'full upgrade') {
388                    $lines[-1]{end} = $event->{timestamp} if @lines;
389                } elsif (($event->{type}||'') eq 'individual upgrade') {
390                    $lines[-1]{sub_events}[-1]{end} = $event->{timestamp}
391                        if @lines and @{ $lines[-1]{sub_events} };
392                }
393            } elsif ($ids{$event->{individual_id}}) {
394                my $end = $event;
395                $event = $ids{$event->{individual_id}};
396                $event->{end} = $end->{timestamp};
397
398                $end->{return_value} = [ split ', ', $end->{return_value}, 2 ]
399                    if $end->{return_value} and not ref $end->{return_value};
400                $event->{return_value} = $end->{return_value};
401                $event->{content} ||= $end->{content};
402            }
403        }
404    }
405
406    return ($version_status, @lines);
407}
408
409=head2 ExternalStorage
410
411Accessor for the storage engine selected by L<RT::ExternalStorage>. Will
412be undefined if external storage is not configured.
413
414=cut
415
416sub ExternalStorage {
417    my $self = shift;
418    if (@_) {
419        $self->{ExternalStorage} = shift;
420    }
421    return $self->{ExternalStorage};
422}
423
424=head2 ExternalStorageURLFor object
425
426Returns a URL for direct linking to an L<RT::ExternalStorage>
427engine. Will return C<undef> if external storage is not configured, or
428if direct linking is disabled in config (C<$ExternalStorageDirectLink>),
429or if the external storage engine doesn't support hyperlinking (as in
430L<RT::ExternalStorage::Disk>), or finally, if the object is for whatever
431reason not present in external storage.
432
433=cut
434
435sub ExternalStorageURLFor {
436    my $self = shift;
437    my $Object = shift;
438
439    # external storage not configured
440    return undef if !$self->ExternalStorage;
441
442    # external storage direct links disabled
443    return undef if !RT->Config->Get('ExternalStorageDirectLink');
444
445    return undef unless $Object->ContentEncoding eq 'external';
446
447    return $self->ExternalStorage->DownloadURLFor($Object);
448}
449
450RT::Base->_ImportOverlays();
451
4521;
453