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
49use strict;
50use warnings;
51
52package RT::Catalog;
53use base 'RT::Record';
54
55use Role::Basic 'with';
56with "RT::Record::Role::Lifecycle",
57     "RT::Record::Role::Roles" => {
58         -rename => {
59             # We provide ACL'd wraps of these.
60             AddRoleMember    => "_AddRoleMember",
61             DeleteRoleMember => "_DeleteRoleMember",
62             RoleGroup        => "_RoleGroup",
63         },
64     },
65     "RT::Record::Role::Rights";
66
67require RT::ACE;
68
69=head1 NAME
70
71RT::Catalog - A logical set of assets
72
73=cut
74
75# For the Lifecycle role
76sub LifecycleType { "asset" }
77
78# Setup rights
79__PACKAGE__->AddRight( General => ShowCatalog  => 'See catalogs' ); #loc
80__PACKAGE__->AddRight( Admin   => AdminCatalog => 'Create, modify, and disable catalogs' ); #loc
81
82__PACKAGE__->AddRight( General => ShowAsset    => 'See assets' ); #loc
83__PACKAGE__->AddRight( Staff   => CreateAsset  => 'Create assets' ); #loc
84__PACKAGE__->AddRight( Staff   => ModifyAsset  => 'Modify assets' ); #loc
85
86__PACKAGE__->AddRight( General => SeeCustomField        => 'View custom field values' ); # loc
87__PACKAGE__->AddRight( Staff   => ModifyCustomField     => 'Modify custom field values' ); # loc
88__PACKAGE__->AddRight( Staff   => SetInitialCustomField => 'Add custom field values only at object creation time'); # loc
89
90RT::ACE->RegisterCacheHandler(sub {
91    my %args = (
92        Action      => "",
93        RightName   => "",
94        @_
95    );
96
97    return unless $args{Action}    =~ /^(Grant|Revoke)$/i
98              and $args{RightName} =~ /^(ShowCatalog|CreateAsset)$/;
99
100    RT::Catalog->CacheNeedsUpdate(1);
101});
102
103=head1 DESCRIPTION
104
105Catalogs are for assets what queues are for tickets or classes are for
106articles.
107
108It announces the rights for assets, and rights are granted at the catalog or
109global level.  Asset custom fields are either applied globally to all Catalogs
110or individually to specific Catalogs.
111
112=over 4
113
114=item id
115
116=item Name
117
118Limited to 255 characters.
119
120=item Description
121
122Limited to 255 characters.
123
124=item Lifecycle
125
126=item Disabled
127
128=item Creator
129
130=item Created
131
132=item LastUpdatedBy
133
134=item LastUpdated
135
136=back
137
138All of these are readable through methods of the same name and mutable through
139methods of the same name with C<Set> prefixed.  The last four are automatically
140managed.
141
142=head1 METHODS
143
144=head2 Load ID or NAME
145
146Loads the specified Catalog into the current object.
147
148=cut
149
150sub Load {
151    my $self = shift;
152    my $id   = shift;
153    return unless $id;
154
155    if ( $id =~ /\D/ ) {
156        return $self->LoadByCols( Name => $id );
157    }
158    else {
159        return $self->SUPER::Load($id);
160    }
161}
162
163=head2 Create PARAMHASH
164
165Create takes a hash of values and creates a row in the database.  Available keys are:
166
167=over 4
168
169=item Name
170
171=item Description
172
173=item Lifecycle
174
175=item HeldBy, Contact
176
177A single principal ID or array ref of principal IDs to add as members of the
178respective role groups for the new catalog.
179
180User Names and EmailAddresses may also be used, but Groups must be referenced
181by ID.
182
183=item Disabled
184
185=back
186
187Returns a tuple of (status, msg) on failure and (id, msg, non-fatal errors) on
188success, where the third value is an array reference of errors that occurred
189but didn't prevent creation.
190
191=cut
192
193sub Create {
194    my $self = shift;
195    my %args = (
196        Name            => '',
197        Description     => '',
198        Lifecycle       => 'assets',
199
200        HeldBy          => undef,
201        Contact         => undef,
202
203        Disabled        => 0,
204
205        @_
206    );
207    my @non_fatal_errors;
208
209    return (0, $self->loc("Permission Denied"))
210        unless $self->CurrentUserHasRight('AdminCatalog');
211
212    return (0, $self->loc('Invalid Name (names must be unique and may not be all digits)'))
213        unless $self->ValidateName( $args{'Name'} );
214
215    $args{'Lifecycle'} ||= 'assets';
216
217    return (0, $self->loc('[_1] is not a valid lifecycle', $args{'Lifecycle'}))
218        unless $self->ValidateLifecycle( $args{'Lifecycle'} );
219
220    RT->DatabaseHandle->BeginTransaction();
221
222    my ( $id, $msg ) = $self->SUPER::Create(
223        map { $_ => $args{$_} } qw(Name Description Lifecycle Disabled),
224    );
225    unless ($id) {
226        RT->DatabaseHandle->Rollback();
227        return (0, $self->loc("Catalog create failed: [_1]", $msg));
228    }
229
230    # Create role groups
231    unless ($self->_CreateRoleGroups()) {
232        RT->Logger->error("Couldn't create role groups for catalog ". $self->id);
233        RT->DatabaseHandle->Rollback();
234        return (0, $self->loc("Couldn't create role groups for catalog"));
235    }
236
237    # Figure out users for roles
238    my $roles = {};
239    push @non_fatal_errors, $self->_ResolveRoles( $roles, %args );
240    push @non_fatal_errors, $self->_AddRolesOnCreate( $roles, map { $_ => sub {1} } $self->Roles );
241
242    # Create transaction
243    my ( $txn_id, $txn_msg, $txn ) = $self->_NewTransaction( Type => 'Create' );
244    unless ($txn_id) {
245        RT->DatabaseHandle->Rollback();
246        return (0, $self->loc( 'Catalog Create txn failed: [_1]', $txn_msg ));
247    }
248
249    $self->CacheNeedsUpdate(1);
250    RT->DatabaseHandle->Commit();
251
252    return ($id, $self->loc('Catalog #[_1] created: [_2]', $self->id, $args{'Name'}), \@non_fatal_errors);
253}
254
255=head2 ValidateName NAME
256
257Requires that Names contain at least one non-digit and doesn't already exist.
258
259=cut
260
261sub ValidateName {
262    my $self = shift;
263    my $name = shift;
264    return 0 unless defined $name and length $name;
265    return 0 unless $name =~ /\D/;
266
267    my $catalog = RT::Catalog->new( RT->SystemUser );
268    $catalog->Load($name);
269    return 0 if $catalog->id;
270
271    return 1;
272}
273
274=head2 Delete
275
276Catalogs may not be deleted.  Always returns failure.
277
278You should disable the catalog instead using C<< $catalog->SetDisabled(1) >>.
279
280=cut
281
282sub Delete {
283    my $self = shift;
284    return (0, $self->loc("Catalogs may not be deleted"));
285}
286
287=head2 CurrentUserCanSee
288
289Returns true if the current user can see the catalog via the I<ShowCatalog> or
290I<AdminCatalog> rights.
291
292=cut
293
294sub CurrentUserCanSee {
295    my $self = shift;
296    return $self->CurrentUserHasRight('ShowCatalog')
297        || $self->CurrentUserHasRight('AdminCatalog');
298}
299
300=head2 Owner
301
302Returns an L<RT::User> object for this catalog's I<Owner> role group.  On error,
303returns undef.
304
305=head2 HeldBy
306
307Returns an L<RT::Group> object for this catalog's I<HeldBy> role group.  The object
308may be unloaded if permissions aren't satisfied.
309
310=head2 Contacts
311
312Returns an L<RT::Group> object for this catalog's I<Contact> role
313group.  The object may be unloaded if permissions aren't satisfied.
314
315=cut
316
317sub Owner {
318    my $self  = shift;
319    my $group = $self->RoleGroup("Owner");
320    return unless $group and $group->id;
321    return $group->UserMembersObj->First;
322}
323sub HeldBy   { $_[0]->RoleGroup("HeldBy")  }
324sub Contacts { $_[0]->RoleGroup("Contact") }
325
326=head2 AddRoleMember
327
328Checks I<AdminCatalog> before calling L<RT::Record::Role::Roles/_AddRoleMember>.
329
330=cut
331
332sub AddRoleMember {
333    my $self = shift;
334
335    return (0, $self->loc("No permission to modify this catalog"))
336        unless $self->CurrentUserHasRight("AdminCatalog");
337
338    return $self->_AddRoleMember(@_);
339}
340
341=head2 DeleteRoleMember
342
343Checks I<AdminCatalog> before calling L<RT::Record::Role::Roles/_DeleteRoleMember>.
344
345=cut
346
347sub DeleteRoleMember {
348    my $self = shift;
349
350    return (0, $self->loc("No permission to modify this catalog"))
351        unless $self->CurrentUserHasRight("AdminCatalog");
352
353    return $self->_DeleteRoleMember(@_);
354}
355
356=head2 RoleGroup
357
358An ACL'd version of L<RT::Record::Role::Roles/_RoleGroup>.  Checks I<ShowCatalog>.
359
360=cut
361
362sub RoleGroup {
363    my $self = shift;
364    if ($self->CurrentUserCanSee) {
365        return $self->_RoleGroup(@_);
366    } else {
367        return RT::Group->new( $self->CurrentUser );
368    }
369}
370
371=head2 AssetCustomFields
372
373Returns an L<RT::CustomFields> object containing all global and
374catalog-specific B<asset> custom fields.
375
376=cut
377
378sub AssetCustomFields {
379    my $self = shift;
380    my $cfs  = RT::CustomFields->new( $self->CurrentUser );
381    if ($self->CurrentUserCanSee) {
382        $cfs->SetContextObject( $self );
383        $cfs->LimitToGlobalOrObjectId( $self->Id );
384        $cfs->LimitToLookupType( RT::Asset->CustomFieldLookupType );
385        $cfs->ApplySortOrder;
386    } else {
387        $cfs->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
388    }
389    return ($cfs);
390}
391
392=head1 INTERNAL METHODS
393
394=head2 CacheNeedsUpdate
395
396Takes zero or one arguments.
397
398If a true argument is provided, marks any Catalog caches as needing an update.
399This happens when catalogs are created, disabled/enabled, or modified.  Returns
400nothing.
401
402If no arguments are provided, returns an epoch time that any catalog caches
403should be newer than.
404
405May be called as a class or object method.
406
407=cut
408
409sub CacheNeedsUpdate {
410    my $class  = shift;
411    my $update = shift;
412
413    if ($update) {
414        RT->System->SetAttribute(Name => 'CatalogCacheNeedsUpdate', Content => time);
415        return;
416    } else {
417        my $attribute = RT->System->FirstAttribute('CatalogCacheNeedsUpdate');
418        return $attribute ? $attribute->Content : 0;
419    }
420}
421
422=head1 PRIVATE METHODS
423
424Documented for internal use only, do not call these from outside RT::Catalog
425itself.
426
427=head2 _Set
428
429Checks if the current user can I<AdminCatalog> before calling C<SUPER::_Set>
430and records a transaction against this object if C<SUPER::_Set> was
431successful.
432
433=cut
434
435sub _Set {
436    my $self = shift;
437    my %args = (
438        Field => undef,
439        Value => undef,
440        @_
441    );
442
443    return (0, $self->loc("Permission Denied"))
444        unless $self->CurrentUserHasRight('AdminCatalog');
445
446    my $old = $self->_Value( $args{'Field'} );
447
448    my ($ok, $msg) = $self->SUPER::_Set(@_);
449
450    # Only record the transaction if the _Set worked
451    return ($ok, $msg) unless $ok;
452
453    my $txn_type = "Set";
454    if ($args{'Field'} eq "Disabled") {
455        if (not $old and $args{'Value'}) {
456            $txn_type = "Disabled";
457        }
458        elsif ($old and not $args{'Value'}) {
459            $txn_type = "Enabled";
460        }
461    }
462
463    $self->CacheNeedsUpdate(1);
464
465    my ($txn_id, $txn_msg, $txn) = $self->_NewTransaction(
466        Type     => $txn_type,
467        Field    => $args{'Field'},
468        NewValue => $args{'Value'},
469        OldValue => $old,
470    );
471    return ($txn_id, scalar $txn->BriefDescription);
472}
473
474=head2 Lifecycle [CONTEXT_OBJ]
475
476Returns the current value of Lifecycle.
477
478Provide an optional asset object as context to check role-level rights
479in addition to catalog-level rights for ShowCatalog and AdminCatalog.
480
481(In the database, Lifecycle is stored as varchar(32).)
482=cut
483
484sub Lifecycle {
485    my $self    = shift;
486    my $context_obj = shift;
487
488    if ( $context_obj && $context_obj->CatalogObj->Id eq $self->Id &&
489        ( $context_obj->CurrentUserHasRight('ShowCatalog') or $context_obj->CurrentUserHasRight('AdminCatalog') ) ) {
490        return ( $self->__Value('Lifecycle') );
491    }
492
493    return ( $self->_Value('Lifecycle') );
494}
495
496=head2 _Value
497
498Checks L</CurrentUserCanSee> before calling C<SUPER::_Value>.
499
500=cut
501
502sub _Value {
503    my $self = shift;
504    return unless $self->CurrentUserCanSee;
505    return $self->SUPER::_Value(@_);
506}
507
508sub Table { "Catalogs" }
509
510sub _CoreAccessible {
511    {
512        id            => { read => 1, type => 'int(11)',        default => '' },
513        Name          => { read => 1, type => 'varchar(255)',   default => '',          write => 1 },
514        Description   => { read => 1, type => 'varchar(255)',   default => '',          write => 1 },
515        Lifecycle     => { read => 1, type => 'varchar(32)',    default => 'assets',    write => 1 },
516        Disabled      => { read => 1, type => 'int(2)',         default => '0',         write => 1 },
517        Creator       => { read => 1, type => 'int(11)',        default => '0', auto => 1 },
518        Created       => { read => 1, type => 'datetime',       default => '',  auto => 1 },
519        LastUpdatedBy => { read => 1, type => 'int(11)',        default => '0', auto => 1 },
520        LastUpdated   => { read => 1, type => 'datetime',       default => '',  auto => 1 },
521    }
522}
523
524sub FindDependencies {
525    my $self = shift;
526    my ($walker, $deps) = @_;
527
528    $self->SUPER::FindDependencies($walker, $deps);
529
530    # Role groups( HeldBy, Contact)
531    my $objs = RT::Groups->new( $self->CurrentUser );
532    $objs->Limit( FIELD => 'Domain', VALUE => 'RT::Catalog-Role', CASESENSITIVE => 0 );
533    $objs->Limit( FIELD => 'Instance', VALUE => $self->Id );
534    $deps->Add( in => $objs );
535
536    # Custom Fields on assets _in_ this catalog
537    $objs = RT::ObjectCustomFields->new( $self->CurrentUser );
538    $objs->Limit( FIELD           => 'ObjectId',
539                  OPERATOR        => '=',
540                  VALUE           => $self->id,
541                  ENTRYAGGREGATOR => 'OR' );
542    $objs->Limit( FIELD           => 'ObjectId',
543                  OPERATOR        => '=',
544                  VALUE           => 0,
545                  ENTRYAGGREGATOR => 'OR' );
546    my $cfs = $objs->Join(
547        ALIAS1 => 'main',
548        FIELD1 => 'CustomField',
549        TABLE2 => 'CustomFields',
550        FIELD2 => 'id',
551    );
552    $objs->Limit( ALIAS    => $cfs,
553                  FIELD    => 'LookupType',
554                  OPERATOR => 'STARTSWITH',
555                  VALUE    => 'RT::Catalog-' );
556    $deps->Add( in => $objs );
557
558    # Assets
559    $objs = RT::Assets->new( $self->CurrentUser );
560    $objs->Limit( FIELD => "Catalog", VALUE => $self->Id );
561    $objs->{allow_deleted_search} = 1;
562    $deps->Add( in => $objs );
563
564}
565
566sub PreInflate {
567    my $class = shift;
568    my ( $importer, $uid, $data ) = @_;
569
570    $class->SUPER::PreInflate( $importer, $uid, $data );
571    $data->{Name} = $importer->Qualify( $data->{Name} );
572
573    return if $importer->MergeBy( "Name", $class, $uid, $data );
574    return 1;
575}
576
577RT::Base->_ImportOverlays();
578
5791;
580