KGRKJGETMRETU895U-589TY5MIGM5JGB5SDFESFREWTGR54TY
Server : Apache/2.4.62
System : FreeBSD fbsdweb2.web.rcn.net 14.1-RELEASE FreeBSD 14.1-RELEASE releng/14.1-n267679-10e31f0946d8 GENERIC amd64
User : www ( 80)
PHP Version : 8.3.8
Disable Function : NONE
Directory :  /domains/compasssysweb/calendar/CalciumDir39/Calendar/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /domains/compasssysweb/calendar/CalciumDir39/Calendar/Event.pm
# Copyright 1999-2003, Fred Steinberg, Brown Bear Software

# Event

# An Event has a string to display, and a link for either a URL or
# Javascript popup. It can also, optionally, have a start time, end time,
# and/or RepeatInfo object for repeating. And then there are other things, too.

# Methods include:
#  new
#  getTimeString
#  isRepeating
#  getHTML
#  applies
#  equals
#  excludeThisInstance
#  addToDateHash
#  getIncludedOverrides ($incInfo) - check for included color, border overrides
#  getCategoryOverrides ($prefs,)  - check for included color, border overrides
#  private - return true if event is not to be included in other cals
#  privatePopup - return true if popup text is not to be included in other cals

# The Get/Set methods are handled by AUTOLOAD.

package Event;
use strict;
use Calendar::EventSorter;
use vars qw ($AUTOLOAD %validField);

use overload ('=='       => 'equals',
              'fallback' => 'true');

# If mod_perling, This had better be a constant hash.
# Date and TZoffset and Prefs are not stored!
BEGIN {
    foreach (qw(text link popup export repeatInfo startTime endTime id owner
                drawBorder bgColor fgColor includedFrom category isTentative
                mailTo mailCC mailBCC mailText timePeriod
                Date TZoffset Prefs
                reminderTimes reminderTo subscriptions)) {
        $validField{$_}++;
    }
}

# Pass hash pairs
sub new {
    my $class = shift;
    my %args = ('text'       => '',
                'link'       => '',
                'popup'      => '',
                'export'     => 'Public',
#                repeatInfo => ,    Notice that
#                startTime  => ,            these keys (and others)
#                endTime    => ,                 are optional
                @_);
    my $self = {};
    bless $self, $class;

    my ($key, $value);
    while (($key, $value) = (each %args)) {
        $self->{$key} = $value if $value;
    }

    # some are special; false is ok, undef is not. silly.
    foreach (qw/startTime endTime/) {
        $self->{$_} = $args{$_} if defined ($args{$_});
    }

    # If a Time Period defined, don't store start/end times
    if ($args{timePeriod}) {
        delete $self->{startTime};
        delete $self->{endTime};
    }

    $self;
}

sub AUTOLOAD {
    my $self = shift;
    my $name = $AUTOLOAD;
    $name =~ s/.*://;           # get rid of package names, etc.
    return unless $name =~ /[^A-Z]/;  # ignore all cap methods; e.g. DESTROY 

    # Make sure it's a valid field, eh wot?
    die "Bad Field Name to Event! '$name'\n" unless $validField{$name};

    $self->{$name} = shift if (@_);
    $self->{$name};
}

# Shallow copy; has references to same thingys (e.g. repeatInfo)
sub copy {
    my $self = shift;
    return Event->new (%$self);
}

# Return passed in text, possibly with HTML possibly escaped and things
# that look something like URLs converted to href links. Newlines are
# converted to <br>, unless simplistic check for HTML tags succeeds.
sub _escapeThis {
    my ($text, $escaped, $doHREFs) = @_;
    return '' unless $text;
    my $noBR = ($text =~ /<[^>]*>/); # not very good, but good enough
    if ($escaped) {
        $text =~ s/</&lt;/g;
        $text =~ s/>/&gt;/g;
    }
    # If there's not href or img in there already, put in href and mailto: tags
    if ($doHREFs and $text !~ /<\s*(a\s+)|(img)/i) {
        $text =~ s {([-\w.]+@[-\w]+\.[-\w.]+)}
                   {<a href="mailto:$1">$1</a>}g;

        my $target = '';
        $target = ' target="_blank"' if ($doHREFs =~ /newwindow/i);
        local $^W = undef;
        # look for 'http' first, then for 'www' if no 'http' found
        $text =~ s {http(s)?://(\S+)}
                   {<a href="http$1://$2"$target>$2</a>}g
          or $text =~ s {(www\.[\w-]+\.[\w-]+)}
                        {<a href="http://$1"$target>$1</a>}g
    }

    $text =~ s/\n/<br>/g unless $noBR;
    $text;
}

# args are flags for ($escapeIt, $doHREFs)
sub escapedText {
    my $self = shift;
    _escapeThis ($self->text, @_);
}
sub escapedPopup {
    my $self = shift;
    _escapeThis ($self->popup, @_);
}

# See if event text matches passed in regex (use quotemeta for exact match).
# 2nd param is where to look; one of ['text', 'popup', 'both']
sub matchesText {
    my ($self, $regex, $where) = @_;
    return undef unless defined $regex;
    $where ||= 'both';
    if ($where =~ /text|both/i) {
        return 1 if (defined $self->text and $self->text =~ /$regex/);
    }
    if ($where =~ /popup|both/i) {
        return 1 if (defined $self->popup and $self->popup =~ /$regex/);
        return 1 if (defined $self->link  and $self->link  =~ /$regex/);
    }
    return undef;
}

# See if event is in any of the categories (typically for filtering)
# Pass one category, or ref to list of them.
sub inCategory {
    my ($self, $cats) = @_;
    $cats = [$cats] unless ref $cats;
    my $myCat = $self->category;
    return undef unless defined $myCat;
    foreach my $cat (@$cats) {
        return 1 if ($myCat eq $cat);
    }
    return undef;
}

sub hasTime {
    my $self = shift;
    return 1 if ($self->timePeriod or $self->startTime);
}
# Set or return startTime, which might be defined in a time period
sub startTime {
    my $self = shift;
    $self->{startTime} = shift if (@_);
    return $self->{startTime} if exists ($self->{startTime});
    return undef unless ($self->timePeriod and $self->Prefs);
    $self->_getTimesFromPeriod;
    return $self->{startTime};
}
sub endTime {
    my $self = shift;
    $self->{endTime} = shift if (@_);
    return $self->{endTime} if exists ($self->{endTime});
    return undef unless ($self->timePeriod and $self->Prefs);
    $self->_getTimesFromPeriod;
    return $self->{endTime};
}
sub _getTimesFromPeriod {
    my $self = shift;
    my ($name, $start, $end, $display) =
                          $self->Prefs->getTimePeriod ($self->timePeriod);
    ($self->{startTime}, $self->{endTime}) = ($start, $end);
}

# If object method, pass 'start', 'end', or 'both', and prefs object
#  - If event has no times, returns undef
#  - If event has Time Period, get times for that
#  - If event has start but no end and you ask for both, returns just start
# If class method, just pass a time (as int) and militaryTimeP
sub getTimeString {
    my $caller = shift;
    my (@times, $milTimeP);
    if (ref ($caller)) {
        my ($which, $prefs) = @_;
        $milTimeP = $prefs->MilitaryTime;
        my ($start, $end);
        $start = $caller->startTime;
        $end   = $caller->endTime;
        push @times, $start if ($which =~ /start|both/ and defined $start);
        push @times, $end   if ($which =~ /end|both/   and defined $end);
    } else {
        push @times, shift;
        $milTimeP = shift;
    }
    my ($theString, $secondTime);
    foreach my $time (@times) {
        next if (!defined $time or $time eq '');   # shouldn't happen, but...
        $theString .= ' - ' if ($secondTime++);
        my ($hour, $minute) = (int ($time / 60), $time % 60);
        if ($milTimeP) {
            $theString .= sprintf '%d:%.2d', $hour, $minute;
        } else {
            my $string;
            if ($hour <= 12) {
                # midnight = 0;
                $string = sprintf '%d:%.2d', ($hour ? $hour : 12), $minute;
            } else {
                $string = sprintf '%d:%.2d', $hour - 12, $minute;
            }
            $string .= ($hour > 11) ? 'pm' : 'am';
            $theString .= $string;
        }
    }
    $theString;
}

# Return what date we fall on, adjusted for timezone
# Pass date, offset in hours (as stored in prefs)
sub getDisplayDate {
    my ($self, $date, $offset) = @_;
    my $start = $self->startTime;
    return $date unless (defined $start and $offset);
    $start += $offset * 60;

    if ($start < 0) {
        my $numDays = int ($start/-1440) + 1;     # 1440 = 24* 60
        return $date - $numDays;
    } elsif ($start >= 1440) {
        my $numDays = int ($start/1440);
        return $date + $numDays;
    }
    return $date;

#     return $date - 1 if $start < 0;
#     return $date + 1 if $start >= 24*60;
#     return $date;
}
# Return (startTime, endTime) adjusted by offset.
sub getDisplayTime {
    my ($self, $offset) = @_;
    my ($start, $end) = ($self->startTime, $self->endTime);
    return ($start, $end) unless $offset;
    foreach ($start, $end) {
        next unless defined;
        $_ += $offset * 60;
        $_ %= 1440;
#         if ($_ < 0) {
#             $_ += 24*60;
#         } elsif ($_ >= 24*60) {
#             $_ -= 24*60;
#         }
    }
    return ($start, $end);
}
# Change times, store date
# Return -1 if date decremented, 1 if incremented, 0 if unchanged
sub adjustForTimezone {
    my ($self, $date, $offsetHours) = @_;
    my ($start, $end) = ($self->startTime, $self->endTime);
    my $offset = $offsetHours * 60;
    my $ret = 0;
    return $self->TZoffset if (defined $self->TZoffset);

    if (defined $start) {
        $start += $offset;
        if ($start < 0) {
            $date -= int ($start/-1440) + 1;
            $start %= 1440;
            $ret = -1;
        } elsif ($start >= 24*60) {
            $date += int ($start/1440);
            $start %= 1440;
            $ret = 1;
        }

        $self->startTime ($start);

        if (defined $end) {
            $end += $offset;
            $self->endTime ($end % 1440);
        }
    }

    $self->Date ($date);       # not stored on disk
    $self->TZoffset ($ret);
    return $ret;
}


sub isRepeating {
    my $self = shift;
    return defined $self->{'repeatInfo'};
}

# Return ('', $linkText) if it's a link, ($popupText, '') if not.
sub textToPopupOrLink {
    my ($ref, $text) = @_;
    return ('', '') unless $text;
    if ($text =~ /^((https?|mailto|ftp|file):)|^www\.[^ .]+\.[^ .]/s) {
        return ('', ($1 ? $text : "http://$text"));
    } else {
        return ($text, '');
    }
}

# The popup code is an HTTP link to display the popup
sub getHTML {
    my ($self, $args) = @_;

    die "Unexpected args to Event::getHTML\n" unless (ref $args eq 'HASH');

    my ($op, $calName, $date, $prefs, $i18n, $textFG,
        $eventFace, $eventSize, $timeFace, $timeSize, $textID,
        $incFace, $incSize, $catFace, $catSize, $hideTimes);

    $op        = $args->{op};
    $calName   = $args->{calName};
    $date      = $args->{date};
    $prefs     = $args->{prefs};
    $i18n      = $args->{i18n};
    $textFG    = $args->{textFG};
    $eventFace = $args->{eventFace};
    $eventSize = $args->{eventSize};
    $timeFace  = $args->{timeFace};
    $timeSize  = $args->{timeSize};
    $textID    = $args->{textID};
    $incFace   = $args->{includeFace};
    $incSize   = $args->{includeSize};
    $catFace   = $args->{categoryFace};
    $catSize   = $args->{categorySize};
    $hideTimes = $args->{hideTimes};

    my $htmlText;

    my $tagFG = $textFG ? "color=\"$textFG\"" : '';

    # if displaying tentative event, add a tentative tag
    if ($self->isTentative) {
        $htmlText .= "<i><font $tagFG size=-1>" .
                           $i18n->get ('Pending Approval') . "</font></i><br>";
    }

    # includedFrom could be wrong if db is cached
    if ($self->includedFrom and $self->includedFrom eq $calName) {
        $self->includedFrom (undef);
    }

    if ($self->includedFrom and $textID) {
        my $more = $incFace ? "face=$incFace " : '';
        $more   .= "size=$incSize" if (defined $incSize);
        $htmlText .= "<font $tagFG $more>$textID</font><br>";
    }

    if (defined $self->category) {
        my $cat = $prefs->category ($self->category) ||
                  MasterDB->new->getPreferences->category ($self->category);
        if ($cat and $cat->showName) {
            my $more = $catFace ? "face=$catFace " : '';
            $more   .= "size=$catSize" if (defined $catSize);
            $htmlText .= "<font $tagFG $more>" . $cat->name .'</font><br>';
        }
    }

    my @timeStrings;
    unless ($hideTimes) {
        if (my $period = $self->timePeriod) {
            my ($name, $start, $end, $display) =
                                          $prefs->getTimePeriod ($period);
            $display ||= '';   # in case period doesn't exist
            if ($display eq 'period' or $display eq 'both') {
                push @timeStrings, $name;
            }
            if ($display eq 'times' or $display eq 'both') {
                push @timeStrings, $self->getTimeString ('both', $prefs);
            }
        } elsif ($self->startTime) {
            push @timeStrings, $self->getTimeString ('both', $prefs);
        }
    }

    if (@timeStrings) {
        my $timefont;
        if ($textFG or $timeFace or $timeSize) {
            $timefont = "<font $tagFG " .
                           ($timeFace ? "face='$timeFace' " : '' ) .
                           ($timeSize ? "size=$timeSize"    : '' ) .
                        '>';
        }
        $htmlText .= $timefont if $timefont;
        $htmlText .= join ': ', @timeStrings;
        $htmlText .= '</font>' if $timefont;
        $htmlText .= '<br>';
    }

    my ($font, $endFont);
    if ($textFG or $eventFace or $eventSize) {
        $font  = "<font $tagFG" .
                   ($eventFace ? "face='$eventFace' " : '') .
                   ($eventSize ? "size=$eventSize"    : '') .
                 '>';
        $endFont = '</font>';
    } else {
        $font = $endFont = '';
    }

    # Display event text, or maybe special string (e.g. 'Out of Office')
    if ($self->includedFrom and !$self->public and !$self->privatePopup) {
        $htmlText .= $font . ($self->displayString ($i18n) || '') . $endFont;
    } else {
        # escape HTML if we need to, converting \n to <br>, and maybe
        # detect email address and http links
        my $escapeIt = $prefs->EventHTML =~ /none/;
        my $eventText;

        if ($self->{'link'} and
            (!$self->includedFrom or $self->public)) {
            $eventText = $self->escapedText ($escapeIt);
            $htmlText .= "<a href=\"$self->{'link'}\">" .
                         "$font$eventText$endFont</a>";
        } elsif ($self->{'popup'} and
                 (!$self->includedFrom or $self->public)) {
            $eventText = $self->escapedText ($escapeIt);

            my $id = $self->id;
            my $source = $self->includedFrom || ' ';

            my $width = $prefs->PopupWidth   || 250;
            my $height = $prefs->PopupHeight || 350;

            my $jsParams = "'$calName', '$date', '$id', '$source', " .
                           "'$width', '$height'";
            my $theText = $font . $eventText . $endFont;
            my $jsStuff = qq{<a href="JavaScript:PopupWindow ($jsParams)">} .
                          "$theText</a>";
            $jsStuff =~ s/(["'])/\\$1/g;     # '"
            $htmlText .= qq {
                             <script>
                                 document.write ("$jsStuff");
                             </script>
                            };
            my $url = '';
            if ($op) {
                $url = $op->makeURL ({Op     => 'PopupWindow',
                                      ID     => $id,
                                      Date   => $date,
                                      Source => $source,
                                      DoneURL =>
                                          $op->makeURL ({Op => $op->opName})});
            }
            $htmlText .= qq {
                             <noscript>
                             <a href="$url">$theText</a>
                             </noscript>
                            };
        } else {
            $eventText = $self->escapedText ($escapeIt, 'doHREFs');
            $htmlText .= $font . $eventText . $endFont;
        }
    }

    $htmlText;
}

# Return (fg, bg) colors based on event settings, inclusion, category
#   return included colors if included (and override set)
#   else, return event specific colors (if set)
#   else, return category colors (if set)
#   else, return colors from prefs
sub colors {
    my ($self, $calName, $prefs, $noDefault) = @_;

    my ($fgColor, $bgColor, $border, $textID);

    # included calendar colors specified?
    if ($self->includedFrom || '' ne $calName) {
        my ($fg, $bg, $bdr, $id) =
            $self->getIncludedOverrides ($prefs->Includes);
        $fgColor = $fg unless defined $fgColor;
        $bgColor = $bg unless defined $bgColor;
    }
    return ($fgColor, $bgColor) if (defined $fgColor and defined $bgColor);

    # event have it's own colors specified?
    $fgColor = $self->fgColor unless defined $fgColor;
    $bgColor = $self->bgColor unless defined $bgColor;
    return ($fgColor, $bgColor) if (defined $fgColor and defined $bgColor);

    # category colors specified?
    if ($self->category) {
        my ($fg, $bg, $bdr) = $self->getCategoryOverrides ($prefs,
                                          MasterDB->new->getPreferences);
        $fgColor = $fg unless defined $fgColor;
        $bgColor = $bg unless defined $bgColor;
    }
    return ($fgColor, $bgColor) if (defined $fgColor and defined $bgColor);

    return ($fgColor, $bgColor) if $noDefault;

    $fgColor = $prefs->color ('EventFG') if (!defined $fgColor);
    $bgColor = $prefs->color ('EventBG') if (!defined $bgColor);

    return ($fgColor, $bgColor);
}

sub applies {
    my $self = shift;
    my ($date) = @_;

    # return true right away if it's not a repeating event
    return 1 unless $self->isRepeating();

    # otherwise, ask the RepeatInfo object
    return $self->{'repeatInfo'}->applies ($date);
}

sub equals {
    my ($e1, $e2, $backwards) = @_;
    return ($e1->{'id'} == $e2->{'id'});
}

# Use this to keep track of which instances of a repeating event we deleted
sub excludeThisInstance {
    my $self = shift;
    my ($date) = @_;
    # Simply pass it along to the RepeatInfo object, unless there isn't
    # one. 
    if ($self->isRepeating()) {
        $self->{'repeatInfo'}->excludeThisInstance ($date);
    }
}

# Set or Return ref to list of excluded date objs; return undef if not a
#   repeating event
sub exclusionList {
    my $self = shift;
    my $listRef = shift;
    # Simply pass it along to the RepeatInfo object, unless there isn't
    # one. 
    if ($self->isRepeating()) {
        return $self->{'repeatInfo'}->exclusionList ($listRef);
    }
    return;
}

# Pass through to EventSorter
sub sort {
    my ($class, $eventListRef, $sortPref) = @_;
    my $sorter = EventSorter->new (split (',', ($sortPref || ())));
    my $sortedListref = $sorter->sortEvents ($eventListRef);
    return @$sortedListref;
}

# Find all applicable dates for this event, add to the hash passed in.
# Notice that if there is no repeat info, we don't need to do anything.
sub addToDateHash {
    my $self = shift;
    my ($hash, $fromDate, $toDate, $prefs) = @_;

    return unless $self->isRepeating();

    $self->repeatInfo()->addToDateHash ($hash, $fromDate, $toDate,
                                        $self, $prefs);
}

sub getIncludedOverrides
{
    my $self = shift;
    my ($incInfo) = @_;
    my $incCal = $self->includedFrom();
    my ($fgColor, $bgColor, $border, $text);
    if ($incCal &&
        $incInfo->{$incCal}->{'Included'}) {
        $text = $incInfo->{$incCal}->{Text} || '';
        if ($incInfo->{$incCal}->{'Override'}) {
            $fgColor = $incInfo->{$incCal}->{'FG'};
            $bgColor = $incInfo->{$incCal}->{'BG'};
            $border  = $incInfo->{$incCal}->{'Border'} ? 1 : 0;
        }
    }
    ($fgColor, $bgColor, $border, $text);
}

sub getCategoryOverrides
{
    my $self = shift;
    my (@prefs) = @_;
    my $catName = $self->category;
    return undef unless $catName;
    foreach my $prefs (@prefs) {
        my $cat = $prefs->category ($catName);
        return ($cat->fg, $cat->bg, $cat->border) if $cat;
    }
    undef;
}

sub public {
    my $self = shift;
    return (!defined $self->export or
            $self->export =~ /Public/i or
            $self->export eq '');
}

sub private {
    my $self = shift;
    return ($self->export and $self->export =~ /Private/i);
}

sub privatePopup {
    my $self = shift;
    return ($self->export and $self->export =~ /NoPopup/i);
}

sub displayString {
    my $self = shift;
    my $i18n = shift;
    return $i18n->get ('Private')       if ($self->private);
    return $i18n->get ('Private Popup') if ($self->privatePopup);
    return '' unless $self->export;
    return $i18n->get ('Unavailable')   if ($self->export =~ /Unavailable/i);
    return $i18n->get ('Out of Office') if ($self->export =~ /OutOfOffice/i);
    return '';
}

# -- Subscriptions
# 'subscriptions' looks like: "calname:[email protected],[email protected];otherCal:[email protected]"
# Need to specify which calendar we're interested in

sub isSubscribed {
    my ($self, $address, $calName) = @_;
    my $addrs = $self->getSubscribers ($calName); # comma joined list
    return ($addrs =~ /\b$address\b/i);
}

sub addSubscriber {
    my ($self, $address, $calName) = @_;
    my @calStrings = split /;/, ($self->subscriptions || '');
    my $foundIt;
    foreach (@calStrings) {
        next unless /^$calName:(.*)/;
        $foundIt = 1;
        $_ .= ',' if $1;
        $_ .= $address;
        last;
    }
    if (!$foundIt) {
        push @calStrings, "$calName:$address";
    }
    $self->subscriptions (join ';', @calStrings);
}

# Return comma joined list of addresses for specified calendar.
sub getSubscribers {
    my ($self, $calName) = @_;
    my @calStrings = split /;/, ($self->subscriptions || '');
    foreach (@calStrings) {
        next unless /^$calName:/;
        s/$calName://;
        return $_;
    }
    return '';
}



{
    # these are the fields that can get stored
    my %map = (a => 'text',
               b => 'link',
               c => 'popup',
               d => 'export',
               e => 'startTime',
               f => 'endTime',
               g => 'id',
               h => 'owner',
               i => 'drawBorder',
               j => 'bgColor',
               k => 'fgColor',
               l => 'mailTo',
               m => 'mailCC',
               n => 'mailBCC',
               o => 'mailText',
               p => 'reminderTimes',
               q => 'reminderTo',
               r => 'category',
               s => 'isTentative',
               t => 'subscriptions',
               u => 'timePeriod',

               A => 'startDate',
               B => 'endDate',
               C => 'period',
               D => 'frequency',
               E => 'monthWeek',
               F => 'monthMonth',
               G => 'exclusions',
               H => 'skipWeekends');

    # Return a list of ascii elements representing an event. Escape newlines.
    sub serialize {
        my $self = shift;
        my @list;

        if ($self->timePeriod) {
            delete $self->{startTime};
            delete $self->{endTime};
        }

        foreach ('a'..'u') {
            my $val = $self->{$map{$_}};
            push @list, ($_, $val) if (defined $val);
        }

        if ($self->isRepeating) {
            foreach ('A'..'H') {
                my $val = $self->{'repeatInfo'}->{$map{$_}};
                push @list, ($_, $val) if (defined $val);
            }
        }

        for (my $i=1; $i<@list; $i+=2) {
            if (ref ($list[$i]) eq 'Date') {
                $list[$i] = "$list[$i]";
              # some fields could be lists (period, monthWeek, exclusions)
              # store them separated by whitespace
            } elsif (ref ($list[$i]) eq 'ARRAY') {
                $list[$i] = join ' ', @{$list[$i]};
            } else {
                $list[$i] =~ s/\n/\\n/g;
                $list[$i] =~ s/\r//g; # otherwise it writes carriage returns
            }
        }
        @list;
    }

    sub unserialize {
        my $classname = shift;
        my (%values) = @_;

        my $self = {};
        bless $self, $classname;

        my $val;
        foreach ('a'..'u') {
            next unless defined ($val = $values{$_});
            ($self->{$map{$_}} = $val) =~ s/\\n/\n/g;
        }

        if ($values{'A'}) {     # startDate
            require Calendar::RepeatInfo;
            $self->{repeatInfo} = RepeatInfo->new (@values{'A'..'F','H'});
            if ($values{'G'}) {
                require Calendar::Date;
                my @exclusions = split /\s+/, $values{'G'};
                @exclusions = map {Date->new($_)} @exclusions;
                $self->{repeatInfo}->{'exclusions'} = \@exclusions;
            }
        }
        $self;
    }
}

1;

Anon7 - 2021