]> git.ipfire.org Git - thirdparty/bugzilla.git/blame - whine.pl
add a new hook: template_after_create (#60)
[thirdparty/bugzilla.git] / whine.pl
CommitLineData
9f3d18d4 1#!/usr/bin/perl -T
fe2a998b
FB
2# This Source Code Form is subject to the terms of the Mozilla Public
3# License, v. 2.0. If a copy of the MPL was not distributed with this
4# file, You can obtain one at http://mozilla.org/MPL/2.0/.
f6c796ad 5#
fe2a998b
FB
6# This Source Code Form is "Incompatible With Secondary Licenses", as
7# defined by the Mozilla Public License, v. 2.0.
f6c796ad 8
9################################################################################
10# Script Initialization
11################################################################################
12
1001cbba 13use 5.10.1;
f6c796ad 14use strict;
9f3d18d4
FB
15use warnings;
16
415e32d4 17use lib qw(. lib);
f6c796ad 18
7f446ae8 19use Bugzilla;
f6c796ad 20use Bugzilla::Constants;
21use Bugzilla::Search;
22use Bugzilla::User;
2b9f5bf8 23use Bugzilla::Mailer;
e4fa5195 24use Bugzilla::Util;
ca06c579 25use Bugzilla::Group;
f6c796ad 26
27# create some handles that we'll need
28my $template = Bugzilla->template;
29my $dbh = Bugzilla->dbh;
30my $sth;
31
2f9f28d0 32# @seen_schedules is a list of all of the schedules that have already been
33# touched by reset_timer. If reset_timer sees a schedule more than once, it
34# sets it to NULL so it won't come up again until the next execution of
35# whine.pl
36my @seen_schedules = ();
37
f6c796ad 38# These statement handles should live outside of their functions in order to
39# allow the database to keep their SQL compiled.
40my $sth_run_queries =
41 $dbh->prepare("SELECT " .
10272c25 42 "query_name, title, onemailperbug " .
f6c796ad 43 "FROM whine_queries " .
44 "WHERE eventid=? " .
45 "ORDER BY sortkey");
46my $sth_get_query =
47 $dbh->prepare("SELECT query FROM namedqueries " .
48 "WHERE userid = ? AND name = ?");
49
50# get the event that's scheduled with the lowest run_next value
51my $sth_next_scheduled_event = $dbh->prepare(
52 "SELECT " .
53 " whine_schedules.eventid, " .
54 " whine_events.owner_userid, " .
55 " whine_events.subject, " .
0665a744 56 " whine_events.body, " .
57 " whine_events.mailifnobugs " .
f6c796ad 58 "FROM whine_schedules " .
59 "LEFT JOIN whine_events " .
60 " ON whine_events.id = whine_schedules.eventid " .
61 "WHERE run_next <= NOW() " .
b9402d3e 62 "ORDER BY run_next " .
63 $dbh->sql_limit(1)
f6c796ad 64);
65
66# get all pending schedules matching an eventid
67my $sth_schedules_by_event = $dbh->prepare(
cc73e8e4 68 "SELECT id, mailto_type, mailto " .
f6c796ad 69 "FROM whine_schedules " .
70 "WHERE eventid=? AND run_next <= NOW()"
71);
72
73
74################################################################################
75# Main Body Execution
76################################################################################
77
78# This script needs to check through the database for schedules that have
79# run_next set to NULL, which means that schedule is new or has been altered.
80# It then sets it to run immediately if the schedule entry has it running at
81# an interval like every hour, otherwise to the appropriate day and time.
82
83# After that, it looks over each user to see if they have schedules that need
84# running, then runs those and generates the email messages.
85
eedbd699 86# Send whines from the address in the 'mailfrom' Parameter so that all
5d6b815d 87# Bugzilla-originated mail appears to come from a single address.
eedbd699 88my $fromaddress = Bugzilla->params->{'mailfrom'};
f6c796ad 89
7a4bbc15 90# get the current date and time
91my ($now_sec, $now_minute, $now_hour, $now_day, $now_month, $now_year,
92 $now_weekday) = localtime;
93# Convert year to two digits
94$now_year = sprintf("%02d", $now_year % 100);
95# Convert the month to January being "1" instead of January being "0".
96$now_month++;
63dde600 97
f6c796ad 98my @daysinmonth = qw(0 31 28 31 30 31 30 31 31 30 31 30 31);
99# Alter February in case of a leap year. This simple way to do it only
100# applies if you won't be looking at February of next year, which whining
101# doesn't need to do.
102if (($now_year % 4 == 0) &&
103 (($now_year % 100 != 0) || ($now_year % 400 == 0))) {
104 $daysinmonth[2] = 29;
105}
106
107# run_day can contain either a calendar day (1, 2, 3...), a day of the week
108# (Mon, Tue, Wed...), a range of days (All, MF), or 'last' for the last day of
109# the month.
110#
111# run_time can contain either an hour (0, 1, 2...) or an interval
112# (60min, 30min, 15min).
113#
114# We go over each uninitialized schedule record and use its settings to
115# determine what the next time it runs should be
116my $sched_h = $dbh->prepare("SELECT id, run_day, run_time " .
117 "FROM whine_schedules " .
118 "WHERE run_next IS NULL" );
119$sched_h->execute();
120while (my ($schedule_id, $day, $time) = $sched_h->fetchrow_array) {
121 # fill in some defaults in case they're blank
122 $day ||= '0';
123 $time ||= '0';
124
125 # If this schedule is supposed to run today, we see if it's supposed to be
126 # run at a particular hour. If so, we set it for that hour, and if not,
127 # it runs at an interval over the course of a day, which means we should
128 # set it to run immediately.
129 if (&check_today($day)) {
130 # Values that are not entirely numeric are intervals, like "30min"
131 if ($time !~ /^\d+$/) {
132 # set it to now
133 $sth = $dbh->prepare( "UPDATE whine_schedules " .
134 "SET run_next=NOW() " .
135 "WHERE id=?");
136 $sth->execute($schedule_id);
137 }
138 # A time greater than now means it still has to run today
139 elsif ($time >= $now_hour) {
140 # set it to today + number of hours
02a04970
MKA
141 $sth = $dbh->prepare(
142 "UPDATE whine_schedules " .
143 "SET run_next = " .
144 $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'HOUR') .
145 " WHERE id = ?");
f6c796ad 146 $sth->execute($time, $schedule_id);
147 }
148 # the target time is less than the current time
149 else { # set it for the next applicable day
e4fa5195 150 $day = &get_next_date($day);
02a04970
MKA
151 my $run_next = $dbh->sql_date_math('('
152 . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY')
153 . ')', '+', '?', 'HOUR');
e4fa5195 154 $sth = $dbh->prepare("UPDATE whine_schedules " .
02a04970
MKA
155 "SET run_next = $run_next
156 WHERE id = ?");
e4fa5195 157 $sth->execute($day, $time, $schedule_id);
f6c796ad 158 }
159
160 }
161 # If the schedule is not supposed to run today, we set it to run on the
162 # appropriate date and time
163 else {
164 my $target_date = &get_next_date($day);
165 # If configured for a particular time, set it to that, otherwise
166 # midnight
167 my $target_time = ($time =~ /^\d+$/) ? $time : 0;
168
02a04970
MKA
169 my $run_next = $dbh->sql_date_math('('
170 . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY')
171 . ')', '+', '?', 'HOUR');
e4fa5195 172 $sth = $dbh->prepare("UPDATE whine_schedules " .
02a04970
MKA
173 "SET run_next = $run_next
174 WHERE id = ?");
f6c796ad 175 $sth->execute($target_date, $target_time, $schedule_id);
176 }
177}
178$sched_h->finish();
179
180# get_next_event
181#
182# This function will:
183# 1. Lock whine_schedules
184# 2. Grab the most overdue pending schedules on the same event that must run
185# 3. Update those schedules' run_next value
186# 4. Unlock the table
187# 5. Return an event hashref
188#
189# The event hashref consists of:
190# eventid - ID of the event
191# author - user object for the event's creator
192# users - array of user objects for recipients
193# subject - Subject line for the email
194# body - the text inserted above the bug lists
0665a744 195# mailifnobugs - send message even if there are no query or query results
f6c796ad 196
197sub get_next_event {
198 my $event = {};
199
200 # Loop until there's something to return
201 until (scalar keys %{$event}) {
202
bd370f37 203 $dbh->bz_start_transaction();
f6c796ad 204
205 # Get the event ID for the first pending schedule
206 $sth_next_scheduled_event->execute;
207 my $fetched = $sth_next_scheduled_event->fetch;
208 $sth_next_scheduled_event->finish;
209 return undef unless $fetched;
0665a744 210 my ($eventid, $owner_id, $subject, $body, $mailifnobugs) = @{$fetched};
f6c796ad 211
9d4872be 212 my $owner = Bugzilla::User->new($owner_id);
f6c796ad 213
214 my $whineatothers = $owner->in_group('bz_canusewhineatothers');
215
216 my %user_objects; # Used for keeping track of who has been added
217
218 # Get all schedules that match that event ID and are pending
219 $sth_schedules_by_event->execute($eventid);
220
221 # Add the users from those schedules to the list
222 while (my $row = $sth_schedules_by_event->fetch) {
cc73e8e4 223 my ($sid, $mailto_type, $mailto) = @{$row};
f6c796ad 224
225 # Only bother doing any work if this user has whine permission
226 if ($owner->in_group('bz_canusewhines')) {
cc73e8e4 227
228 if ($mailto_type == MAILTO_USER) {
229 if (not defined $user_objects{$mailto}) {
230 if ($mailto == $owner_id) {
231 $user_objects{$mailto} = $owner;
232 }
233 elsif ($whineatothers) {
234 $user_objects{$mailto} = Bugzilla::User->new($mailto);
235 }
f6c796ad 236 }
cc73e8e4 237 }
238 elsif ($mailto_type == MAILTO_GROUP) {
239 my $sth = $dbh->prepare("SELECT name FROM groups " .
240 "WHERE id=?");
241 $sth->execute($mailto);
242 my $groupname = $sth->fetch->[0];
243 my $group_id = Bugzilla::Group::ValidateGroupName(
244 $groupname, $owner);
245 if ($group_id) {
9d4872be 246 my $glist = join(',',
ca06c579 247 @{Bugzilla::Group->flatten_group_membership(
0ec82863 248 $group_id)});
cc73e8e4 249 $sth = $dbh->prepare("SELECT user_id FROM " .
250 "user_group_map " .
9d4872be 251 "WHERE group_id IN ($glist)");
252 $sth->execute();
cc73e8e4 253 for my $row (@{$sth->fetchall_arrayref}) {
254 if (not defined $user_objects{$row->[0]}) {
255 $user_objects{$row->[0]} =
256 Bugzilla::User->new($row->[0]);
257 }
258 }
f6c796ad 259 }
260 }
cc73e8e4 261
f6c796ad 262 }
263
264 reset_timer($sid);
265 }
266
bd370f37 267 $dbh->bz_commit_transaction();
f6c796ad 268
269 # Only set $event if the user is allowed to do whining
270 if ($owner->in_group('bz_canusewhines')) {
271 my @users = values %user_objects;
272 $event = {
273 'eventid' => $eventid,
274 'author' => $owner,
275 'mailto' => \@users,
276 'subject' => $subject,
277 'body' => $body,
0665a744 278 'mailifnobugs' => $mailifnobugs,
f6c796ad 279 };
280 }
281 }
282 return $event;
283}
284
285# Run the queries for each event
286#
287# $event:
288# eventid (the database ID for this event)
289# author (user object for who created the event)
290# mailto (array of user objects for mail targets)
291# subject (subject line for message)
292# body (text blurb at top of message)
0665a744 293# mailifnobugs (send message even if there are no query or query results)
f6c796ad 294while (my $event = get_next_event) {
295
296 my $eventid = $event->{'eventid'};
297
298 # We loop for each target user because some of the queries will be using
299 # subjective pronouns
03352ccf 300 $dbh = Bugzilla->switch_to_shadow_db();
f6c796ad 301 for my $target (@{$event->{'mailto'}}) {
302 my $args = {
303 'subject' => $event->{'subject'},
304 'body' => $event->{'body'},
305 'eventid' => $event->{'eventid'},
306 'author' => $event->{'author'},
307 'recipient' => $target,
308 'from' => $fromaddress,
309 };
310
311 # run the queries for this schedule
312 my $queries = run_queries($args);
313
0665a744 314 # If mailifnobugs is false, make sure there is something to output
315 if (!$event->{'mailifnobugs'}) {
316 my $there_are_bugs = 0;
317 for my $query (@{$queries}) {
318 $there_are_bugs = 1 if scalar @{$query->{'bugs'}};
319 }
320 next unless $there_are_bugs;
f6c796ad 321 }
f6c796ad 322
323 $args->{'queries'} = $queries;
324
325 mail($args);
326 }
03352ccf 327 $dbh = Bugzilla->switch_to_main_db();
f6c796ad 328}
329
330################################################################################
331# Functions
332################################################################################
333
334# The mail and run_queries functions use an anonymous hash ($args) for their
335# arguments, which are then passed to the templates.
336#
337# When run_queries is run, $args contains the following fields:
338# - body Message body defined in event
339# - from Bugzilla system email address
340# - queries array of hashes containing:
341# - bugs: array of hashes mapping fieldnames to values for this bug
342# - title: text title given to this query in the whine event
fd004da7 343# - columnlist: array of fieldnames to display in the mail
df2bdf81 344# - name: text name of this query
f6c796ad 345# - schedule_id integer id of the schedule being run
346# - subject Subject line for the message
347# - recipient user object for the recipient
348# - author user object of the person who created the whine event
f6c796ad 349sub mail {
350 my $args = shift;
43b7dc31 351 # Don't send mail to someone whose bugmail notification is disabled.
948b5806 352 return if $args->{recipient}->email_disabled;
f6c796ad 353
948b5806
FB
354 $args->{to_user} = $args->{recipient};
355 MessageToMTA(generate_email(
356 $args,
f6c796ad 357 {
948b5806
FB
358 header => 'whine/header.txt.tmpl',
359 text => 'whine/mail.txt.tmpl',
360 html => 'whine/mail.html.tmpl',
361 }
362 ));
f6c796ad 363}
364
365# run_queries runs all of the queries associated with a schedule ID, adding
366# the results to $args or mailing off the template if a query wants individual
367# messages for each bug
368sub run_queries {
369 my $args = shift;
370
371 my $return_queries = [];
372
373 $sth_run_queries->execute($args->{'eventid'});
10272c25 374 my @queries = ();
f6c796ad 375 for (@{$sth_run_queries->fetchall_arrayref}) {
10272c25 376 push(@queries,
377 {
378 'name' => $_->[0],
379 'title' => $_->[1],
380 'onemailperbug' => $_->[2],
fd004da7 381 'columnlist' => [],
10272c25 382 'bugs' => [],
383 }
384 );
f6c796ad 385 }
386
10272c25 387 foreach my $thisquery (@queries) {
f6c796ad 388 next unless $thisquery->{'name'}; # named query is blank
389
390 my $savedquery = get_query($thisquery->{'name'}, $args->{'author'});
391 next unless $savedquery; # silently ignore missing queries
392
393 # Execute the saved query
a84cc300
EW
394 my @searchfields = ('bug_id', DEFAULT_COLUMN_LIST);
395
f6c796ad 396 # A new Bugzilla::CGI object needs to be created to allow
397 # Bugzilla::Search to execute a saved query. It's exceedingly weird,
398 # but that's how it works.
399 my $searchparams = new Bugzilla::CGI($savedquery);
fd004da7
AT
400
401 # Use the columnlist for the saved query, if it exists, and make
402 # sure bug_id is always in the list.
403 if (my $columnlist = $searchparams->param('columnlist')) {
404 @searchfields = split(/[\s,]+/, $columnlist);
405 unshift(@searchfields, 'bug_id') unless grep { $_ eq 'bug_id' } @searchfields;
406 }
407 push @{$thisquery->{'columnlist'}}, @searchfields;
408
409 my @orderstrings = split(/,\s*/, $searchparams->param('order') || '');
f6c796ad 410 my $search = new Bugzilla::Search(
411 'fields' => \@searchfields,
dbaf1c3a 412 'params' => scalar $searchparams->Vars,
f6c796ad 413 'user' => $args->{'recipient'}, # the search runs as the recipient
65b467bd 414 'order' => \@orderstrings
f6c796ad 415 );
c9aaffd4 416 # If a query fails for whatever reason, it shouldn't kill the script.
d8643908 417 my $data = eval { $search->data };
c9aaffd4 418 if ($@) {
acae816c
FB
419 print STDERR get_text('whine_query_failed', { query_name => $thisquery->{'name'},
420 author => $args->{'author'},
421 reason => $@ }) . "\n";
c9aaffd4
FB
422 next;
423 }
424
d8643908 425 foreach my $row (@$data) {
f6c796ad 426 my $bug = {};
427 for my $field (@searchfields) {
428 my $fieldname = $field;
429 $fieldname =~ s/^bugs\.//; # No need for bugs.whatever
d8643908 430 $bug->{$fieldname} = shift @$row;
f6c796ad 431 }
432
433 if ($thisquery->{'onemailperbug'}) {
434 $args->{'queries'} = [
435 {
436 'name' => $thisquery->{'name'},
437 'title' => $thisquery->{'title'},
fd004da7 438 'columnlist' => $thisquery->{'columnlist'},
f6c796ad 439 'bugs' => [ $bug ],
440 },
441 ];
442 mail($args);
443 delete $args->{'queries'};
444 }
445 else { # It belongs in one message with any other lists
446 push @{$thisquery->{'bugs'}}, $bug;
447 }
448 }
b64f45e6 449 if (!$thisquery->{'onemailperbug'} && @{$thisquery->{'bugs'}}) {
f6c796ad 450 push @{$return_queries}, $thisquery;
451 }
452 }
453
454 return $return_queries;
455}
456
457# get_query gets the namedquery. It's similar to LookupNamedQuery (in
458# buglist.cgi), but doesn't care if a query name really exists or not, since
459# individual named queries might go away without the whine_queries that point
460# to them being removed.
461sub get_query {
462 my ($name, $user) = @_;
463 my $qname = $name;
1c51e402 464 $sth_get_query->execute($user->id, $qname);
f6c796ad 465 my $fetched = $sth_get_query->fetch;
466 $sth_get_query->finish;
467 return $fetched ? $fetched->[0] : '';
468}
469
470# check_today gets a run day from the schedule and sees if it matches today
471# a run day value can contain any of:
472# - a three-letter day of the week
473# - a number for a day of the month
474# - 'last' for the last day of the month
475# - 'All' for every day
476# - 'MF' for every weekday
477
478sub check_today {
479 my $run_day = shift;
480
481 if (($run_day eq 'MF')
482 && ($now_weekday > 0)
483 && ($now_weekday < 6)) {
484 return 1;
485 }
486 elsif (
487 length($run_day) == 3 &&
488 index("SunMonTueWedThuFriSat", $run_day)/3 == $now_weekday) {
489 return 1;
490 }
491 elsif (($run_day eq 'All')
492 || (($run_day eq 'last') &&
493 ($now_day == $daysinmonth[$now_month] ))
494 || ($run_day eq $now_day)) {
495 return 1;
496 }
497 return 0;
498}
499
500# reset_timer sets the next time a whine is supposed to run, assuming it just
501# ran moments ago. Its only parameter is a schedule ID.
502#
503# reset_timer does not lock the whine_schedules table. Anything that calls it
504# should do that itself.
505sub reset_timer {
506 my $schedule_id = shift;
507
2f9f28d0 508 # Schedules may not be executed more than once for each invocation of
509 # whine.pl -- there are legitimate circumstances that can cause this, like
510 # a set of whines that take a very long time to execute, so it's done
511 # quietly.
4f5cad99 512 if (grep($_ == $schedule_id, @seen_schedules)) {
2f9f28d0 513 null_schedule($schedule_id);
4f5cad99 514 return;
2f9f28d0 515 }
516 push @seen_schedules, $schedule_id;
517
f6c796ad 518 $sth = $dbh->prepare( "SELECT run_day, run_time FROM whine_schedules " .
519 "WHERE id=?" );
520 $sth->execute($schedule_id);
521 my ($run_day, $run_time) = $sth->fetchrow_array;
522
4c2d4f95 523 # It may happen that the run_time field is NULL or blank due to
524 # a bug in editwhines.cgi when this field was initially 0.
525 $run_time ||= 0;
526
f6c796ad 527 my $run_today = 0;
528 my $minute_offset = 0;
529
530 # If the schedule is to run today, and it runs many times per day,
531 # it shall be set to run immediately.
532 $run_today = &check_today($run_day);
533 if (($run_today) && ($run_time !~ /^\d+$/)) {
534 # The default of 60 catches any bad value
535 my $minute_interval = 60;
536 if ($run_time =~ /^(\d+)min$/i) {
537 $minute_interval = $1;
538 }
539
540 # set the minute offset to the next interval point
541 $minute_offset = $minute_interval - ($now_minute % $minute_interval);
542 }
543 elsif (($run_today) && ($run_time > $now_hour)) {
544 # timed event for later today
545 # (This should only happen if, for example, an 11pm scheduled event
546 # didn't happen until after midnight)
547 $minute_offset = (60 * ($run_time - $now_hour)) - $now_minute;
548 }
549 else {
550 # it's not something that runs later today.
551 $minute_offset = 0;
552
553 # Set the target time if it's a specific hour
554 my $target_time = ($run_time =~ /^\d+$/) ? $run_time : 0;
555
556 my $nextdate = &get_next_date($run_day);
02a04970
MKA
557 my $run_next = $dbh->sql_date_math('('
558 . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY')
559 . ')', '+', '?', 'HOUR');
e4fa5195 560 $sth = $dbh->prepare("UPDATE whine_schedules " .
02a04970
MKA
561 "SET run_next = $run_next
562 WHERE id = ?");
f6c796ad 563 $sth->execute($nextdate, $target_time, $schedule_id);
564 return;
565 }
566
f6c796ad 567 if ($minute_offset > 0) {
e4fa5195 568 # Scheduling is done in terms of whole minutes.
02a04970
MKA
569
570 my $next_run = $dbh->selectrow_array(
571 'SELECT ' . $dbh->sql_date_math('NOW()', '+', '?', 'MINUTE'),
572 undef, $minute_offset);
e4fa5195 573 $next_run = format_time($next_run, "%Y-%m-%d %R");
574
f6c796ad 575 $sth = $dbh->prepare("UPDATE whine_schedules " .
e4fa5195 576 "SET run_next = ? WHERE id = ?");
577 $sth->execute($next_run, $schedule_id);
f6c796ad 578 } else {
579 # The minute offset is zero or less, which is not supposed to happen.
2f9f28d0 580 # complain to STDERR
581 null_schedule($schedule_id);
582 print STDERR "Error: bad minute_offset for schedule ID $schedule_id\n";
f6c796ad 583 }
584}
585
2f9f28d0 586# null_schedule is used to safeguard against infinite loops. Schedules with
587# run_next set to NULL will not be available to get_next_event until they are
588# rescheduled, which only happens when whine.pl starts.
589sub null_schedule {
590 my $schedule_id = shift;
591 $sth = $dbh->prepare("UPDATE whine_schedules " .
592 "SET run_next = NULL " .
593 "WHERE id=?");
594 $sth->execute($schedule_id);
595}
596
f6c796ad 597# get_next_date determines the difference in days between now and the next
598# time a schedule should run, excluding today
599#
600# It takes a run_day argument (see check_today, above, for an explanation),
e4fa5195 601# and returns an integer, representing a number of days.
f6c796ad 602sub get_next_date {
603 my $day = shift;
604
605 my $add_days = 0;
606
607 if ($day eq 'All') {
608 $add_days = 1;
609 }
610 elsif ($day eq 'last') {
611 # next_date should contain the last day of this month, or next month
612 # if it's today
613 if ($daysinmonth[$now_month] == $now_day) {
614 my $month = $now_month + 1;
615 $month = 1 if $month > 12;
616 $add_days = $daysinmonth[$month] + 1;
617 }
618 else {
619 $add_days = $daysinmonth[$now_month] - $now_day;
620 }
621 }
622 elsif ($day eq 'MF') { # any day Monday through Friday
623 if ($now_weekday < 5) { # Sun-Thurs
624 $add_days = 1;
625 }
626 elsif ($now_weekday == 5) { # Friday
627 $add_days = 3;
628 }
629 else { # it's 6, Saturday
630 $add_days = 2;
631 }
632 }
633 elsif ($day !~ /^\d+$/) { # A specific day of the week
634 # The default is used if there is a bad value in the database, in
635 # which case we mark it to a less-popular day (Sunday)
636 my $day_num = 0;
637
638 if (length($day) == 3) {
639 $day_num = (index("SunMonTueWedThuFriSat", $day)/3) or 0;
640 }
641
642 $add_days = $day_num - $now_weekday;
2f9f28d0 643 if ($add_days <= 0) { # it's next week
f6c796ad 644 $add_days += 7;
645 }
646 }
647 else { # it's a number, so we set it for that calendar day
648 $add_days = $day - $now_day;
649 # If it's already beyond that day this month, set it to the next one
4f5cad99 650 if ($add_days <= 0) {
f6c796ad 651 $add_days += $daysinmonth[$now_month];
652 }
653 }
e4fa5195 654 return $add_days;
f6c796ad 655}