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