whine.pl 24 KB

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