tagging: Allow using titles in for related stories.
[matthijs/upstream/blosxom-plugins.git] / general / calendar
1 # Blosxom Plugin: calendar                                         -*- perl -*-
2 # Author: Todd Larason (jtl@molehill.org)
3 # Version: 0+6i
4 # Blosxom Home/Docs/Licensing: http://blosxom.sourceforge.net/
5 # Calendar plugin Home/Docs/Licensing:
6 #   http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/Calendar/
7
8 package calendar;
9
10 # --- Configuration Variables ---
11
12 @monthname = qw/January February March 
13                 April   May      June 
14                 July    August   September 
15                 October November December/ if ($#monthname != 11);
16 @monthabbr = qw/Jan    Feb       Mar 
17                 Apr    May       Jun 
18                 Jul    Aug       Sep  
19                 Oct    Nov       Dec/ if ($#monthabbr != 11);
20 @downame   = qw/Sunday Monday    Tuesday Wednesday Thursday 
21                 Friday Saturday/ if ($#downame != 6);
22 @dowabbr = qw/Sun Mon Tue Wed Thu Fri Sat/ if ($#dowabbr != 6);
23
24 $first_dow = 0
25     if not defined $first_dow;
26
27 # set to 0 to disable attempted caching
28 $use_caching    = 1 unless defined $use_caching;
29 $months_per_row = 3 unless defined $months_per_row;
30 $debug_level    = 0 unless defined $debug_level;
31 # -------------------------------------------------------------------
32
33 use Time::Local;
34
35 $month_calendar  = '';
36 $year_calendar   = '';
37 $calendar        = '';
38 $prev_month_link = '';
39 $next_month_link = '';
40 $prev_year_link  = '';
41 $next_year_link  = '';
42
43 my $package    = "calendar";
44 my $cachefile  = "$blosxom::plugin_state_dir/.$package.cache";
45 my $cache;
46 my $save_cache = 0;
47 my $files;
48 \f
49 sub debug {
50     my ($level, @msg) = @_;
51
52     if ($debug_level >= $level) {
53         print STDERR "$package debug $level: @msg\n";
54     }
55     1;
56 }
57
58 sub load_template {
59     my ($bit) = @_;
60     return $blosxom::template->('', "$package.$bit", $blosxom::flavour);
61 }
62
63 sub report {
64     my ($bit, $year, $month, $day, $dow) = @_;
65     my ($monthname, $monthabbr) = ($monthname[$month-1], $monthabbr[$month-1]);
66     my ($downame,   $dowabbr)   = ($downame[$dow],       $dowabbr[$dow]);
67     my $year2digit = sprintf("%02d", $year % 100);
68
69     my $url = $blosxom::url;
70     $url   .= sprintf("/%04d/", $year) if defined  $year;
71     $url   .= sprintf("%02d/", $month) if defined $month;
72     $url   .= sprintf("%02d/",   $day) if defined   $day;
73
74     my $date = '';
75     $date .= "$year"   if defined  $year;
76     $date .= "/$month" if defined $month;
77     $date .= "/$day"   if defined   $day;
78     my $count = $cache->{stories}{$date};
79
80     my $f = load_template($bit);
81     $f =~ s/((\$[\w:]+)|(\$\{[\w:]+\}))/$1 . "||''"/gee;
82     return $f;
83 }
84
85 sub days_in_month {
86     my ($year, $month) = @_;
87     my $days = (31,28,31,30,31,30,31,31,30,31,30,31)[$month-1];
88     if ($month == 2 &&
89         ($year % 4 == 0 &&
90          (($year % 100 != 0) ||
91           ($year % 400 == 0)))) {
92         $days++;
93     }
94     return $days;
95 }
96
97 sub pseudo_now {
98   my ($year, $month, $day);
99
100   if (defined($blosxom::path_info_yr)) {
101     $year  = $blosxom::path_info_yr     + 0;
102     $month = $blosxom::path_info_mo_num + 0;
103     $day   = $blosxom::path_info_da     + 0;
104     if ($month == 0 && $cache->{stories}{$year}) {
105         for ($month = 12; $month > 0; $month--) {
106             last if $cache->{stories}{"$year/$month"};
107         }
108     }
109     $month ||= 12;
110   } else {
111       my $now;
112       # is this a single-article view? 
113       # XXX this probably doesn't work for static
114       if ($blosxom::path_info =~ m!\.!) {
115         my $filename = $blosxom::path_info;
116         # replace whatever flavour with the file extension
117         $filename =~ s!\..*!.$blosxom::file_extension!;
118         # remove any dated dirs, if present
119         $filename =~ s!/\d+/.*/!/!;
120         # and convert from an URL relative to a filename
121         $filename = "$blosxom::datadir/$filename";
122         # at this point, it's our best guess at a filename
123         # if it's not in the index, oh well, we tried - fall back to present
124         $now = $files->{$filename} || $^T;
125       } else {
126         $now = $^T;
127       }
128       # no date given at all, do this month and highlight today
129       my @now = localtime($now);
130       $year   = $now[5] + 1900;
131       $month  = $now[4] + 1;
132       $day    = $now[3] + 0;
133     }
134     return ($year, $month, $day);
135 }
136
137 sub build_prev_month_link {
138   my ($year, $month) = @_;
139   my $year_orig  = $year;
140   my $month_orig = $month;
141
142   while (1) {
143     $month--;
144     if ($month <= 0) { # == 0 is right, <= 2xprotects against infinite loop bug
145       $year--;
146       $month = 12;
147       # XXX assumption: once a log is active, no full years are skipped
148       if ($cache->{stories}{"$year"} == 0) {
149           return report('prev_month_nolink',
150                         $month_orig == 1 ? $year_orig-1 : $year_orig,
151                         $month_orig == 1 ? 12           : $month_orig-1);
152       }
153     }
154     if ($cache->{stories}{"$year/$month"}) {
155       return report('prev_month_link', $year, $month);
156     }
157   }
158 }
159
160 sub build_next_month_link {
161   my ($year, $month) = @_;
162   my $year_orig  = $year;
163   my $month_orig = $month;
164
165   while (1) {
166     $month++;
167     if ($month == 13) {
168       $year++;
169       $month = 1;
170       # XXX assumption: once a log is active, no full years are skipped
171       if ($cache->{stories}{"$year"} == 0) {
172           return report('next_month_nolink',
173                         $month_orig == 1 ? $year_orig-1 : $year_orig,
174                         $month_orig == 1 ? 12           : $month_orig-1);
175       }
176     }
177     if ($cache->{stories}{"$year/$month"}) {
178         return report('next_month_link', $year, $month);
179     }
180   }
181 }
182
183 sub build_prev_year_link {
184   my ($year) = @_;
185
186   $year--;
187   return report($cache->{stories}{"$year"} ? 
188                 'prev_year_link': 'prev_year_nolink',
189                 $year);
190 }
191
192 sub build_next_year_link {
193   my ($year) = @_;
194   my $results;
195
196   $year++;
197   return report($cache->{stories}{"$year"} ? 
198                 'next_year_link': 'next_year_nolink',
199                 $year);
200 }
201
202 sub build_month_calendar {
203     my ($year, $month, $highlight_dom) = @_;
204     my $results;
205
206     my (@now, $monthstart, @monthstart);
207     my ($day, $days, $wday);
208     my $future_dom = 0;
209
210     @now = localtime($^T);
211     $future_dom = $now[3]+1 if ($year == $now[5]+1900 && $month == $now[4]+1);
212
213     $days       = days_in_month($year, $month);
214     $monthstart = timelocal(0,0,0,1,$month-1,$year-1900);
215     @monthstart = localtime($monthstart);
216
217     $results  = report('month_head',        $year, $month);
218     $results .= report('month_sub_head',    $year, $month);
219     $wday = $first_dow;
220     do {
221         $results .= report('month_sub_day', $year, $month, undef, $wday);
222         $wday++;
223         $wday %= 7;
224     } while ($wday != $first_dow);
225     $results .= report('month_sub_foot',    $year, $month);
226
227     # First, skip over the first partial week (possibly empty)
228     # before the month started
229     for ($wday = $first_dow; $wday != $monthstart[6]; $wday++, $wday %= 7) {
230         $results .= report('week_head',     $year, $month) 
231             if ($wday == $first_dow);
232         $results .= report('noday',         $year, $month, undef, $wday);
233     }
234
235     # now do the month itself
236     for ($day = 1; $day <= $days; $day++) {
237         $results .= report('week_head',     $year, $month, $day) 
238             if ($wday == $first_dow);
239         my $tag;
240         if ($day == $highlight_dom) {
241             if ($cache->{stories}{"$year/$month/$day"}){$tag='this_day_link'}
242             else {$tag = 'this_day_nolink'}} 
243         elsif ($cache->{stories}{"$year/$month/$day"}){$tag = 'day_link'}
244         elsif ($future_dom && $day >= $future_dom) {$tag = 'day_future'} 
245         else {$tag = 'day_nolink'}
246         $results .= report($tag,            $year, $month, $day, $wday);
247         $wday = 0 if (++$wday == 7);
248         $results .= report('week_foot',     $year, $month)
249             if ($wday == $first_dow);
250     }
251
252     # and finish up the last week, if any left
253     if ($wday != $first_dow) {
254         for(; $wday != $first_dow; $wday++, $wday %= 7) {
255             $results .= report('noday',     $year, $month, undef, $wday);
256         }
257         $results .= report('week_foot',     $year, $month);
258     }
259
260     $results .= report('month_foot',        $year, $month);
261
262     return $results;
263 }
264
265 sub build_year_calendar {
266     my ($year, $highlight_month) = @_;
267     my $results;
268     my $month;
269     my $future_month = 0;
270
271     @now = localtime($^T);
272     $future_month = $now[4]+1 if ($year == $now[5]+1900);
273
274     $results = report('year_head', $year);
275     for ($month = 1; $month <= 12; $month++) {
276         $results .= report('quarter_head', $year) 
277             if ($month % $months_per_row == 1);
278         my $tag;
279         if ($month == $highlight_month) {
280             if ($cache->{stories}{"$year/$month"}) {$tag = 'this_month_link'}
281             else {$tag = 'this_month_nolink'}}
282         elsif ($cache->{stories}{"$year/$month"}) {$tag = 'month_link'} 
283         elsif ($future_month && $month >=$future_month){$tag = 'month_future'} 
284         else {$tag = 'month_nolink'}
285         $results .= report($tag, $year, $month);
286         $results .= report('quarter_foot', $year) 
287             if ($month % $months_per_row == 0);
288     }
289     $results .= report('year_foot', $year);
290
291     return $results;
292 }
293
294 sub build_calendar {
295   return report('calendar');
296 }
297
298 \f
299 sub cached {
300     my ($sub, $cachetag, @args) = @_;
301     my $cachekey = join '/',@args,$blosxom::flavour;
302     return $cache->{$cachetag}{$cachekey} ||= 
303         (debug(1, "cache miss $cachetag @args $blosxom::flavour") and
304          ($save_cache = 1) and 
305          $sub->(@args));
306 }
307
308 sub prime_cache {
309     my ($num_files) = @_;
310     return 0 if !$use_caching;
311     eval "require Storable";
312     if ($@) {
313         debug(1, "cache disabled, Storable not available"); 
314         $use_caching = 0; 
315         return 0;
316     }
317     if (!Storable->can('lock_retrieve')) {
318         debug(1, "cache disabled, Storable::lock_retrieve not available");
319         $use_caching = 0;
320         return 0;
321     }
322     $cache = (-r $cachefile ? Storable::lock_retrieve($cachefile) : undef);
323     # >= rather than ==, so that if we're being used along with a search
324     # plugin that reduces %files, rather than dumping the cache and showing
325     # a limited calendar, we'll display the full thing (if available) .  I 
326     # think that's preferable as well as being more efficient.
327     # XXX improvement: rather than dumping the whole thing, just update the
328     # count and dump the current month (and sometimes the previous month
329     # and year)
330     @now = localtime;
331     $now[4] += 1;
332     $now[5] += 1900;
333     $today = "$now[5]/$now[4]/$now[3]";
334     if ($cache && 
335         $cache->{num_files} >= $num_files && 
336         $cache->{today} eq $today) {
337         debug(1, "Using cached state");
338         return 1;
339     }
340     $cache = {num_files => $num_files, today => $today};
341     return 0;
342 }
343
344 sub save_cache {
345     return if (!$use_caching || !$save_cache);
346     debug(1, "Saving cache");
347     -d $blosxom::plugin_state_dir
348         or mkdir $blosxom::plugin_state_dir;
349     Storable::lock_store($cache, $cachefile);
350 }
351 \f
352 sub start {
353     debug(1, "start() called, enabled");
354
355     while (<DATA>) {
356         chomp;
357         last if /^(__END__)?$/;
358         my ($flavour, $comp, $txt) = split ' ',$_,3;
359         $txt =~ s:\\n:\n:g;
360         $blosxom::template{$flavour}{"$package.$comp"} = $txt;
361     }
362     return 1;
363 }
364
365 sub filter {
366   my ($pkg, $files_ref) = @_;
367   debug(1, "filter() called");
368
369   $files = $files_ref;
370
371   my $num_files = scalar keys %$files;
372   my @latest = (sort {$b <=> $a} values %$files);
373   while ($latest[0] == $^T) {
374       $num_files--;
375       shift @latest;
376   }
377   return 1 if prime_cache($num_files);
378
379   debug(1, "cache miss: %stories");
380
381   foreach (keys %{$files}) {
382     next if ($_ == $^T);
383     my @date  = localtime($files->{$_});
384     my $mday  = $date[3];
385     my $month = $date[4] + 1;
386     my $year  = $date[5] + 1900;
387     $cache->{stories}{"$year"}++;
388     $cache->{stories}{"$year/$month"}++;
389     $cache->{stories}{"$year/$month/$mday"}++;
390   }
391   debug(1, "filter() done");
392   return 1;
393 }
394
395 sub head {
396   debug(1, "head() called");
397   my ($year, $month, $day) = pseudo_now();
398   if ($year < 1970) {
399       debug(0,"Bad year $year requested ($year, path_info $ENV{PATH_INFO}, year to 2000");
400       $year = 2000;
401   }
402   $prev_month_link = cached(\&build_prev_month_link, "pml", $year,$month);
403   $next_month_link = cached(\&build_next_month_link, "nml", $year,$month);
404   $prev_year_link  = cached(\&build_prev_year_link,  "pyl", $year);
405   $next_year_link  = cached(\&build_next_year_link,  "nyl", $year);
406   $month_calendar  = cached(\&build_month_calendar,  "mc",  $year,$month,$day);
407   $year_calendar   = '';
408   for (my $y = (localtime)[5]+1900; $cache->{stories}{$y}; $y--) {
409       my $varname = "year_calendar_$y";
410       if ($y == $year) {
411           $year_calendar = cached(\&build_year_calendar, "yc",$year,$month);
412           $$varname      = $year_calendar;
413       } else {
414           $$varname      = cached(\&build_year_calendar, "yc", $y);
415       }
416   }
417   $year_calendar   = cached(\&build_year_calendar,   "yc",  $year,$month)
418       unless $year_calendar;
419   $calendar        = cached(\&build_calendar,        "c",   $year,$month,$day);
420  
421   save_cache();
422   
423   debug(1, "head() done, length(\$month_calendar, \$year_calendar, \$calendar) = ", length($month_calendar), length($year_calendar), length($calendar));
424   return 1;
425 }
426
427 # these look better (to me), but don't use <caption> like they 'should', and
428 # the year one assumes 3 columns
429 #html month_head <table class="month-calendar"><tr class="month-calendar-head"><th align="left">$prev_month_link</th><th colspan="5"><a title="$monthname $year" href="$url">$monthname</a></th><th align="right">$next_month_link</th></tr>\n
430 #html year_head <table class="year-calendar"><tr class="year-calendar-head"><th align="left">$prev_year_link</th><th><a title="$year" href="$url">$year</a></th><th align="right">$next_year_link</th></tr><tr><th class="year-calendar-subhead" colspan=$months_per_row>Months</th></tr>\n
431
432 1;
433 __DATA__
434 error month_head <table class="month-calendar"><caption class="month-calendar-head">$prev_month_link<a title="$monthname $year ($count)" href="$url">$monthname</a>$next_month_link</caption>\n
435 error month_sub_head <tr>\n
436 error month_sub_day <th class="month-calendar-day-head $downame">$dowabbr</th>\n
437 error month_sub_foot </tr>\n
438 error week_head <tr>\n
439 error noday <td class="month-calendar-day-noday $downame">&nbsp;</td>\n
440 error day_link <td class="month-calendar-day-link $downame"><a title="$downame, $day $monthname $year ($count)" href="$url">$day</a></td>\n
441 error day_nolink <td class="month-calendar-day-nolink $downame">$day</td>\n
442 error day_future <td class="month-calendar-day-future $downame">$day</td>\n
443 error this_day_link <td class="month-calendar-day-this-day $downame"><a title="$downame, $day $monthname $year (current) ($count)" href="$url">$day</a></td>\n
444 error this_day_nolink <td class="month-calendar-day-this-day $downame">$day</td>\n
445 error week_foot </tr>\n
446 error month_foot </table>\n
447 error prev_month_link <a title="$monthname $year ($count)" href="$url">&larr;</a>
448 error next_month_link <a title="$monthname $year ($count)" href="$url">&rarr;</a>
449 error prev_month_nolink &larr;
450 error next_month_nolink &rarr;
451 error year_head <table class="year-calendar"><caption class="year-calendar-head">$prev_year_link<a title="$year ($count)" href="$url">$year</a>$next_year_link</caption><tr><th class="year-calendar-subhead" colspan="$months_per_row">Months</th></tr>\n
452 error quarter_head <tr>\n
453 error month_link <td class="year-calendar-month-link"><a title="$monthname $year ($count)" href="$url">$monthabbr</a></td>\n
454 error month_nolink <td class="year-calendar-month-nolink">$monthabbr</td>\n
455 error month_future <td class="year-calendar-month-future">$monthabbr</td>\n
456 error this_month_link <td class="year-calendar-this-month"><a title="$monthname $year ($count)" href="$url">$monthabbr</a></td>
457 error this_month_nolink <td class="year-calendar-this-month">$monthabbr</td>
458 error quarter_foot </tr>\n
459 error year_foot </table>\n
460 error prev_year_link <a title="$year ($count)" href="$url">&larr;</a>
461 error next_year_link <a title="$year ($count)" href="$url">&rarr;</a>
462 error prev_year_nolink &larr;
463 error next_year_nolink &rarr;
464 error calendar <div class="calendar"><table><tr><td>$calendar::month_calendar</td><td>$calendar::year_calendar</td></tr></table></div>
465 __END__
466
467 =head1 NAME
468
469 Blosxom Plug-in: calendar
470
471 =head1 SYNOPSIS
472
473 Purpose: Provides a Radio-style archive navigation calendar
474
475   * $calendar::calendar -- side-by-side month and year calendars
476   * $calendar::month_calendar -- month calendar only
477   * $calendar::year_calendar -- year calendar only
478   * $calendar::year_calendar_2003 -- year calendar for 2003; these are built for every year with stories.
479
480 =head1 VERSION
481
482 0+6i
483
484 6th test release
485
486 =head1 AUTHOR
487
488 Todd Larason  <jtl@molehill.org>, http://molelog.molehill.org/
489
490 This plugin is now maintained by the Blosxom Sourceforge Team,
491 <blosxom-devel@lists.sourceforge.net>.
492
493 =head1 BUGS
494
495 None known; please send bug reports and feedback to the Blosxom
496 development mailing list <blosxom-devel@lists.sourceforge.net>.
497
498 =head1 Customization
499
500 =head2 Configuration variables
501
502 C<@monthname>, C<@monthabbr>, C<@downame> and C<@dowabbr> contain the
503 long and short forms of the names of the months and days of the week; 
504 @downame and @dowabbr should always start with Sunday, regardless of
505 $first_dow.
506
507 C<$first_dow> sets the plugin's idea of the first day day of the week;
508 use 0 for Sunday, 1 for Monday, and so on; using a number outside the
509 range [0..6] will cause undefined behavior, possibly including a 
510 nuclear meltdown.
511
512 C<$use_caching> controls whether or not to try to cache statistics and
513 formatted results; caching requires Storable, but the plugin will work
514 just fine without it.
515
516 C<$months_per_row> controls how many months are on each row of the
517 year calendar.  This should be a number that evenly divides 12 (1, 2,
518 3, 4, 6 or 12), but nothing checks for that.
519
520 C<$debug_level> can be set to a value between 0 and 5; 0 will output
521 no debug information, while 5 will be very verbose.  The default is 1,
522 and should be changed after you've verified the plugin is working
523 correctly.
524
525 =head2 Classes for CSS control
526
527 There's an (over)abundance of classes used, available for CSS customization.
528
529   * C<calendar> -- the calendar as a whole
530   * C<month-calendar> -- the month calendar as a whole
531   * C<month-calendar-head> -- the head of the month calendar (ie,
532     "March")
533   * C<month-calendar-day-head> -- a column head in the month calendar
534     (ie, a day-of-week abbreviation)
535   * C<month-calendar-day-noday>, C<month-calendar-day-link>,
536     C<month-calendar-day-nolink>, C<month-calendar-day-future>,
537     C<month-calendar-day-this-day> -- the day squares on the month
538     calendar, for days that don't exist (before or after the month
539     itself), that don't have stories, that do have stories, that are
540     in the future, or are that currently selected, respectively
541   * Day-of-week-name -- each day square is also given a class matching
542     its day of week, from C<@downame>; this can be used to hilight
543     weekends
544   * C<year-calendar> -- the year calendar as a whole
545   * C<year-calendar-head> -- the head of the year calendar (ie,
546     "2003")
547   * C<year-calendar-subhead> -- ie, "Months"
548   * C<year-calendar-month-link>, C<year-calendar-month-nolink>,
549     C<year-calendar-month-future>, C<year-calendar-this-month> -- the
550     month squares on the year calendar, for months with stories,
551     without, in the future, and currently selected, respectively.
552
553 =head2 Flavour-style files
554
555 If you want a format change that can't be made by CSS, you can
556 override the HTML generated by creating files similar to Blosxom's
557 flavour files.  They should be named calendar.I<bit>.I<flavour>; for
558 available I<bit>s and their default meanings, see the C<__DATA__>
559 section in the plugin.
560
561 =head1 Installation
562
563 1. Download and unpack (if you're reading this, you've probably
564    already done that)
565 2. Copy it to your plugins directory.  Make sure it's world-readable.
566 3. Modify a C<head> or C<foot> file to include C<$calendar::calendar>,
567    C<$calendar::month_calendar> or C<$calendar::year_calendar>
568 4. Try it out -- load your blog in your browser.  If you see a calendar, great!
569 5. Look at your error log.  Verify you have an 'enabled' line.
570 6. If you're wanting to verify caching is working, load the page
571    again, and now look for an error log line "calendar debug 1: Using
572    cached state"
573 7. Once you're satisfied it's working, edit the C<$debug_level> configuration
574    variable to C<0>.  There are a couple other configuration variables you may 
575    wish to change, too.
576
577 =head1 Caching
578
579 If the Storable module is available and $use_caching is set, various
580 bits of data will be cached; this includes the information on what
581 days have stories (and are thus linkable, and what months and years
582 are included in the next/forward lists), the contents of any flavour
583 files, and the final formatted output of any calendars generated.  
584
585 The cache will be flushed whenever a story is added (but not when one
586 is removed), so in normal use should be invisible.  If you're making
587 template changes however, or are removing stories, you may wish to
588 either disable the cache (by setting $use_caching to 0) or manually
589 flush the cache; this can be done by removing
590 $plugin_state_dir/.calendar.cache, and is always safe to do.
591
592 =head1 LICENSE
593
594 this Blosxom Plug-in
595 Copyright 2003, Todd Larason
596
597 (This license is the same as Blosxom's)
598
599 Permission is hereby granted, free of charge, to any person obtaining a
600 copy of this software and associated documentation files (the "Software"),
601 to deal in the Software without restriction, including without limitation
602 the rights to use, copy, modify, merge, publish, distribute, sublicense,
603 and/or sell copies of the Software, and to permit persons to whom the
604 Software is furnished to do so, subject to the following conditions:
605
606 The above copyright notice and this permission notice shall be included
607 in all copies or substantial portions of the Software.
608
609 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
610 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
611 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
612 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
613 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
614 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
615 OTHER DEALINGS IN THE SOFTWARE.
616
617