streams/include/datetime.php

592 lines
17 KiB
PHP
Raw Normal View History

<?php
2021-12-03 03:01:39 +00:00
2022-01-25 23:25:21 +00:00
2022-02-16 04:08:28 +00:00
use Code\Lib\Features;
use Code\Render\Theme;
2022-02-12 20:43:29 +00:00
2022-01-25 23:25:21 +00:00
/**
* @file include/datetime.php
* @brief Some functions for date and time related tasks.
*/
2010-09-27 07:38:26 +00:00
2013-02-26 01:09:40 +00:00
/**
* @brief Two-level sort for timezones.
*
* Can be used in usort() to sort timezones.
*
* @param string $a
* @param string $b
* @return number
*/
2021-12-03 03:01:39 +00:00
function timezone_cmp($a, $b)
{
if (strstr($a, '/') && strstr($b, '/')) {
if (t($a) == t($b)) {
return 0;
}
return ( t($a) < t($b)) ? -1 : 1;
}
2022-10-21 09:46:29 +00:00
if (str_contains($a, '/')) {
2021-12-03 03:01:39 +00:00
return -1;
}
2022-10-21 09:46:29 +00:00
if (str_contains($b, '/')) {
2021-12-03 03:01:39 +00:00
return 1;
}
if (t($a) == t($b)) {
return 0;
}
return ( t($a) < t($b)) ? -1 : 1;
2013-02-26 01:09:40 +00:00
}
2010-07-01 23:48:07 +00:00
2021-12-03 03:01:39 +00:00
function is_null_date($s)
{
2022-10-21 09:46:29 +00:00
return $s === '0000-00-00 00:00:00' || $s === '0001-01-01 00:00:00';
}
/**
* @brief Return timezones grouped (primarily) by continent.
*
* @see timezone_cmp()
* @return array
*/
2021-12-03 03:01:39 +00:00
function get_timezones()
{
$timezone_identifiers = DateTimeZone::listIdentifiers();
usort($timezone_identifiers, 'timezone_cmp');
$continent = '';
$continents = [];
foreach ($timezone_identifiers as $value) {
$ex = explode("/", $value);
if (count($ex) > 1) {
$continent = t($ex[0]);
if (count($ex) > 2) {
$city = substr($value, strpos($value, '/') + 1);
} else {
$city = $ex[1];
}
} else {
$city = $ex[0];
$continent = t('Miscellaneous');
}
$city = str_replace('_', ' ', t($city));
if (!x($continents, $ex[0])) {
$continents[$ex[0]] = [];
}
$continents[$continent][$value] = $city;
}
return $continents;
2013-02-26 01:09:40 +00:00
}
/**
* @brief General purpose date parse/convert function.
*
* @param string $from source timezone
* @param string $to dest timezone
* @param string $datetime some parseable date/time string
* @param string $format output format recognised from php's DateTime class
* http://www.php.net/manual/en/datetime.format.php
* @return string
*/
function datetime_convert($from = 'UTC', $to = 'UTC', $datetime = 'now', $format = "Y-m-d H:i:s")
2021-12-03 03:01:39 +00:00
{
// Defaults to UTC if nothing is set, but throws an exception if set to empty string.
// Provide some sane defaults regardless.
if ($from === '') {
$from = 'UTC';
}
if ($to === '') {
$to = 'UTC';
}
if (($datetime === '') || (! is_string($datetime))) {
$datetime = 'now';
2021-12-03 03:01:39 +00:00
}
if (is_null_date($datetime)) {
2021-12-03 03:01:39 +00:00
$d = new DateTime('0001-01-01 00:00:00', new DateTimeZone('UTC'));
return $d->format($format);
2021-12-03 03:01:39 +00:00
}
try {
$from_obj = new DateTimeZone($from);
} catch (Exception $e) {
$from_obj = new DateTimeZone('UTC');
}
try {
$d = new DateTime($datetime, $from_obj);
2021-12-03 03:01:39 +00:00
} catch (Exception $e) {
logger('exception: ' . $e->getMessage());
$d = new DateTime('now', $from_obj);
}
try {
$to_obj = new DateTimeZone($to);
} catch (Exception $e) {
$to_obj = new DateTimeZone('UTC');
}
$d->setTimeZone($to_obj);
return($d->format($format));
2013-02-26 01:09:40 +00:00
}
2010-07-01 23:48:07 +00:00
/**
* @brief Wrapper for date selector, tailored for use in birthday fields.
*
* @param string $dob Date of Birth
* @return string Parsed HTML with selector
*/
2021-12-03 03:01:39 +00:00
function dob($dob)
{
2016-03-12 21:13:46 +00:00
2021-12-03 03:01:39 +00:00
$y = substr($dob, 0, 4);
if ((! ctype_digit($y)) || ($y < 1900)) {
$ignore_year = true;
} else {
$ignore_year = false;
}
2021-12-03 03:01:39 +00:00
if ($dob === '0000-00-00' || $dob === '') {
$value = '';
} else {
$value = (($ignore_year) ? datetime_convert('UTC', 'UTC', $dob, 'm-d') : datetime_convert('UTC', 'UTC', $dob, 'Y-m-d'));
}
2021-12-03 03:01:39 +00:00
$age = age($value, App::$user['timezone'], App::$user['timezone']);
2020-07-31 02:38:01 +00:00
2022-02-12 20:43:29 +00:00
$o = replace_macros(Theme::get_template("field_input.tpl"), [
2021-12-03 03:01:39 +00:00
'$field' => [ 'dob', t('Birthday'), $value, ((intval($age)) ? t('Age: ') . $age : ''), '', 'placeholder="' . t('YYYY-MM-DD or MM-DD') . '"' ]
]);
2021-12-03 03:01:39 +00:00
return $o;
}
2014-10-18 20:17:49 +00:00
/**
* @brief Returns a datetime selector.
*
* @param string $format
* format string, e.g. 'ymd' or 'mdy'. Not currently supported
* @param DateTime $min
* unix timestamp of minimum date
* @param DateTime $max
* unix timestap of maximum date
* @param DateTime $default
* unix timestamp of default date
* @param string $label
* @param string $id
* id and name of datetimepicker (defaults to "datetimepicker")
2021-12-02 23:02:31 +00:00
* @param bool $pickdate
* true to show date picker (default)
2021-12-02 23:02:31 +00:00
* @param bool $picktime
* true to show time picker (default)
* @param DateTime $minfrom
* set minimum date from picker with id $minfrom (none by default)
* @param DateTime $maxfrom
* set maximum date from picker with id $maxfrom (none by default)
2021-12-02 23:02:31 +00:00
* @param bool $required default false
* @param int $first_day (optional) default 0
* @return string Parsed HTML output.
*
* @todo Once browser support is better this could probably be replaced with
* native HTML5 date picker.
2014-10-18 20:17:49 +00:00
*/
2021-12-03 03:01:39 +00:00
function datetimesel($format, $min, $max, $default, $label, $id = 'datetimepicker', $pickdate = true, $picktime = true, $minfrom = '', $maxfrom = '', $required = false, $first_day = 0)
{
$o = '';
$dateformat = '';
if ($pickdate) {
$dateformat .= 'Y-m-d';
}
if ($pickdate && $picktime) {
$dateformat .= ' ';
}
if ($picktime) {
$dateformat .= 'H:i';
}
$minjs = $min->getTimestamp() ? ",minDate: new Date({$min->getTimestamp()}*1000), yearStart: " . $min->format('Y') : '';
$maxjs = $max->getTimestamp() ? ",maxDate: new Date({$max->getTimestamp()}*1000), yearEnd: " . $max->format('Y') : '';
$input_text = $default->getTimestamp() ? date($dateformat, $default->getTimestamp()) : '';
$defaultdatejs = $default->getTimestamp() ? ",defaultDate: new Date({$default->getTimestamp()}*1000)" : '';
$pickers = '';
if (!$pickdate) {
$pickers .= ',datepicker: false';
}
if (!$picktime) {
$pickers .= ',timepicker: false, closeOnDateSelect:true';
}
$extra_js = '';
if ($minfrom != '') {
$extra_js .= "\$('#id_$minfrom').data('xdsoft_datetimepicker').setOptions({onChangeDateTime: function (currentDateTime) { \$('#id_$id').data('xdsoft_datetimepicker').setOptions({minDate: currentDateTime})}})";
}
if ($maxfrom != '') {
$extra_js .= "\$('#id_$maxfrom').data('xdsoft_datetimepicker').setOptions({onChangeDateTime: function (currentDateTime) { \$('#id_$id').data('xdsoft_datetimepicker').setOptions({maxDate: currentDateTime})}})";
}
$readable_format = $dateformat;
$readable_format = str_replace('Y', 'yyyy', $readable_format);
$readable_format = str_replace('m', 'mm', $readable_format);
$readable_format = str_replace('d', 'dd', $readable_format);
$readable_format = str_replace('H', 'HH', $readable_format);
$readable_format = str_replace('i', 'MM', $readable_format);
2022-02-12 20:43:29 +00:00
$tpl = Theme::get_template('field_input.tpl');
2022-11-20 06:44:13 +00:00
$o .= replace_macros($tpl, [
'$field' => [$id, $label, $input_text, (($required) ? t('Required') : ''), (($required) ? '*' : ''), 'placeholder="' . $readable_format . '"'],
]);
2023-10-11 19:11:10 +00:00
$o .= "<script>\$(function () {let picker = \$('#id_$id').datetimepicker({step:15,format:'$dateformat' $minjs $maxjs $pickers $defaultdatejs,dayOfWeekStart:$first_day}); $extra_js})</script>";
2021-12-03 03:01:39 +00:00
return $o;
2014-10-18 20:17:49 +00:00
}
2011-06-07 02:59:20 +00:00
/**
* @brief Returns a relative date string.
*
* Implements "3 seconds ago" etc.
* Based on $posted_date, (UTC).
* Results relative to current timezone.
* Limited to range of timestamps.
*
* @param string $posted_date
* @param string $format (optional) parsed with sprintf()
* <tt>%1$d %2$s ago</tt>, e.g. 22 hours ago, 1 minute ago
* @return string with relative date
*/
2021-12-03 03:01:39 +00:00
function relative_date($posted_date, $format = null)
{
$localtime = datetime_convert('UTC', date_default_timezone_get(), $posted_date);
$abs = strtotime($localtime);
if (is_null($posted_date) || is_null_date($posted_date) || $abs === false) {
return t('never');
}
if ($abs > time()) {
$direction = t('from now');
$etime = $abs - time();
} else {
$direction = t('ago');
$etime = time() - $abs;
}
if ($etime < 1) {
return sprintf(t('less than a second %s'), $direction);
}
2022-11-20 06:44:13 +00:00
$a = [12 * 30 * 24 * 60 * 60 => 'y',
2021-12-03 03:01:39 +00:00
30 * 24 * 60 * 60 => 'm',
7 * 24 * 60 * 60 => 'w',
24 * 60 * 60 => 'd',
60 * 60 => 'h',
60 => 'i',
1 => 's'
2022-11-20 06:44:13 +00:00
];
2021-12-03 03:01:39 +00:00
foreach ($a as $secs => $str) {
$d = $etime / $secs;
if ($d >= 1) {
$r = round($d);
if (! $format) {
$format = t('%1$d %2$s %3$s', 'e.g. 22 hours ago, 1 minute ago');
}
return sprintf($format, $r, plural_dates($str, $r), $direction);
}
}
}
2010-09-27 07:38:26 +00:00
2021-12-03 03:01:39 +00:00
function plural_dates($k, $n)
{
2022-11-20 06:44:13 +00:00
return match ($k) {
'y' => tt('year', 'years', $n, 'relative_date'),
'm' => tt('month', 'months', $n, 'relative_date'),
'w' => tt('week', 'weeks', $n, 'relative_date'),
'd' => tt('day', 'days', $n, 'relative_date'),
'h' => tt('hour', 'hours', $n, 'relative_date'),
'i' => tt('minute', 'minutes', $n, 'relative_date'),
's' => tt('second', 'seconds', $n, 'relative_date'),
default => '',
};
}
/**
* @brief Returns timezone correct age in years.
*
* Returns the age in years, given a date of birth, the timezone of the person
* whose date of birth is provided, and the timezone of the person viewing the
* result.
*
* Why? Bear with me. Let's say I live in Mittagong, Australia, and my birthday
* is on New Year's. You live in San Bruno, California.
* When exactly are you going to see my age increase?
*
* A: 5:00 AM Dec 31 San Bruno time. That's precisely when I start celebrating
* and become a year older. If you wish me happy birthday on January 1
* (San Bruno time), you'll be a day late.
*
* @param string $dob Date of Birth
* @param string $owner_tz (optional) timezone of the person of interest
* @param string $viewer_tz (optional) timezone of the person viewing
* @return number
*/
2021-12-03 03:01:39 +00:00
function age($dob, $owner_tz = '', $viewer_tz = '')
{
if (! intval($dob)) {
return 0;
}
if (! $owner_tz) {
$owner_tz = date_default_timezone_get();
}
if (! $viewer_tz) {
$viewer_tz = date_default_timezone_get();
}
$birthdate = datetime_convert('UTC', $owner_tz, $dob . ' 00:00:00+00:00', 'Y-m-d');
list($year,$month,$day) = explode("-", $birthdate);
$year_diff = datetime_convert('UTC', $viewer_tz, 'now', 'Y') - $year;
$curr_month = datetime_convert('UTC', $viewer_tz, 'now', 'm');
$curr_day = datetime_convert('UTC', $viewer_tz, 'now', 'd');
if (($curr_month < $month) || (($curr_month == $month) && ($curr_day < $day))) {
$year_diff--;
}
return $year_diff;
2010-07-10 05:47:32 +00:00
}
2011-02-23 04:08:15 +00:00
/**
* @brief Get days of a month in a given year.
*
* Returns number of days in the month of the given year.
* $m = 1 is 'January' to match human usage.
*
* @param int $y year
* @param int $m month (1=January, 12=December)
* @return int number of days in the given month
*/
2021-12-03 03:01:39 +00:00
function get_dim($y, $m)
{
$dim = [ 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
2021-12-03 03:01:39 +00:00
if ($m != 2) {
return $dim[$m];
}
if (((($y % 4) == 0) && (($y % 100) != 0)) || (($y % 400) == 0)) {
return 29;
}
return $dim[2];
2013-02-26 01:09:40 +00:00
}
2011-02-23 04:08:15 +00:00
/**
* @brief Returns the first day in month for a given month, year.
*
* Months start at 1.
*
* @param int $y Year
* @param int $m Month (1=January, 12=December)
* @return string day 0 = Sunday through 6 = Saturday
*/
2021-12-03 03:01:39 +00:00
function get_first_dim($y, $m)
{
$d = sprintf('%04d-%02d-01 00:00', intval($y), intval($m));
2011-02-23 04:08:15 +00:00
2021-12-03 03:01:39 +00:00
return datetime_convert('UTC', 'UTC', $d, 'w');
2013-02-26 01:09:40 +00:00
}
2011-02-23 04:08:15 +00:00
/**
* @brief Output a calendar for the given month, year.
*
* If $links are provided (array), e.g. $links[12] => 'http://mylink' ,
* date 12 will be linked appropriately. Today's date is also noted by
* altering td class.
* Months count from 1.
*
* @param number $y Year
* @param number $m Month
* @param string $links (default false)
* @param string $class
* @return string
*
* @todo provide (prev,next) links, define class variations for different size calendars
*/
2021-12-03 03:01:39 +00:00
function cal($y = 0, $m = 0, $links = false, $class = '')
{
// month table - start at 1 to match human usage.
$mtab = [ ' ', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];
$thisyear = datetime_convert('UTC', date_default_timezone_get(), 'now', 'Y');
$thismonth = datetime_convert('UTC', date_default_timezone_get(), 'now', 'm');
if (! $y) {
$y = $thisyear;
}
if (! $m) {
$m = intval($thismonth);
}
$dn = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ];
$f = get_first_dim($y, $m);
$l = get_dim($y, $m);
$d = 1;
$dow = 0;
$started = false;
if (($y == $thisyear) && ($m == $thismonth)) {
$tddate = intval(datetime_convert('UTC', date_default_timezone_get(), 'now', 'j'));
}
$str_month = day_translate($mtab[$m]);
$o = '<table class="calendar' . $class . '">';
$o .= "<caption>$str_month $y</caption><tr>";
for ($a = 0; $a < 7; $a++) {
$o .= '<th>' . mb_substr(day_translate($dn[$a]), 0, 3, 'UTF-8') . '</th>';
}
$o .= '</tr><tr>';
while ($d <= $l) {
if (($dow == $f) && (! $started)) {
$started = true;
}
$today = (((isset($tddate)) && ($tddate == $d)) ? "class=\"today\" " : '');
$o .= "<td $today>";
$day = str_replace(' ', '&nbsp;', sprintf('%2.2d', $d));
if ($started) {
if (is_array($links) && isset($links[$d])) {
2022-11-20 06:44:13 +00:00
$o .= "<a href=\"$links[$d]\">$day</a>";
2021-12-03 03:01:39 +00:00
} else {
$o .= $day;
}
$d++;
} else {
$o .= '&nbsp;';
}
$o .= '</td>';
$dow++;
if (($dow == 7) && ($d <= $l)) {
$dow = 0;
$o .= '</tr><tr>';
}
}
if ($dow) {
for ($a = $dow; $a < 7; $a++) {
$o .= '<td>&nbsp;</td>';
}
}
$o .= '</tr></table>' . "\r\n";
return $o;
}
2011-10-14 07:20:37 +00:00
2014-06-03 00:49:19 +00:00
/**
* @brief Return the next birthday, converted from the owner's timezone to UTC.
*
2014-06-03 00:49:19 +00:00
* This makes it globally portable.
* If the provided birthday lacks a month and or day, return an empty string.
* A missing year is acceptable.
*
* @param string $dob Date of Birth
* @param string $tz Timezone
* @param string $format
* @return string
2014-06-03 00:49:19 +00:00
*/
2021-12-03 03:01:39 +00:00
function z_birthday($dob, $tz, $format = "Y-m-d H:i:s")
{
if (! strlen($tz)) {
$tz = 'UTC';
}
$birthday = '';
$tmp_dob = substr($dob, 5);
$tmp_d = substr($dob, 8);
if (intval($tmp_dob) && intval($tmp_d)) {
$y = datetime_convert($tz, $tz, 'now', 'Y');
$bd = $y . '-' . $tmp_dob . ' 00:00';
$t_dob = strtotime($bd);
$now = strtotime(datetime_convert($tz, $tz, 'now'));
if ($t_dob < $now) {
2022-08-15 10:33:19 +00:00
$bd = sprintf("%d-%s 00:00", intval($y) + 1, $tmp_dob);
2021-12-03 03:01:39 +00:00
}
$birthday = datetime_convert($tz, 'UTC', $bd, $format);
}
return $birthday;
}
2014-06-03 00:49:19 +00:00
/**
* @brief Create a birthday event for any connections with a birthday in the next 1-2 weeks.
2014-06-03 00:49:19 +00:00
*
* Update the year so that we don't create another event until next year.
*/
2021-12-03 03:01:39 +00:00
function update_birthdays()
{
2014-06-03 00:49:19 +00:00
2021-12-03 03:01:39 +00:00
require_once('include/event.php');
require_once('include/permissions.php');
2014-06-03 00:49:19 +00:00
2021-12-03 03:01:39 +00:00
$r = q(
"SELECT * FROM abook left join xchan on abook_xchan = xchan_hash
PostgreSQL support initial commit There were 11 main types of changes: - UPDATE's and DELETE's sometimes had LIMIT 1 at the end of them. This is not only non-compliant but it would certainly not do what whoever wrote it thought it would. It is likely this mistake was just copied from Friendica. All of these instances, the LIMIT 1 was simply removed. - Bitwise operations (and even some non-zero int checks) erroneously rely on MySQL implicit integer-boolean conversion in the WHERE clauses. This is non-compliant (and bad programming practice to boot). Proper explicit boolean conversions were added. New queries should use proper conventions. - MySQL has a different operator for bitwise XOR than postgres. Rather than add yet another dba_ func, I converted them to "& ~" ("AND NOT") when turning off, and "|" ("OR") when turning on. There were no true toggles (XOR). New queries should refrain from using XOR when not necessary. - There are several fields which the schema has marked as NOT NULL, but the inserts don't specify them. The reason this works is because mysql totally ignores the constraint and adds an empty text default automatically. Again, non-compliant, obviously. In these cases a default of empty text was added. - Several statements rely on a non-standard MySQL feature (http://dev.mysql.com/doc/refman/5.5/en/group-by-handling.html). These queries can all be rewritten to be standards compliant. Interestingly enough, the newly rewritten standards compliant queries run a zillion times faster, even on MySQL. - A couple of function/operator name translations were needed (RAND/RANDOM, GROUP_CONCAT/STRING_AGG, UTC_NOW, REGEXP/~, ^/#) -- assist functions added in the dba_ - INTERVALs: postgres requires quotes around the value, mysql requires that there are not quotes around the value -- assist functions added in the dba_ - NULL_DATE's -- Postgres does not allow the invalid date '0000-00-00 00:00:00' (there is no such thing as year 0 or month 0 or day 0). We use '0001-01-01 00:00:00' for postgres. Conversions are handled in Zot/item packets automagically by quoting all dates with dbescdate(). - char(##) specifications in the schema creates fields with blank spaces that aren't trimmed in the code. MySQL apparently treats char(##) as varchar(##), again, non-compliant. Since postgres works better with text fields anyway, this ball of bugs was simply side-stepped by using 'text' datatype for all text fields in the postgres schema. varchar was used in a couple of places where it actually seemed appropriate (size constraint), but without rigorously vetting that all of the PHP code actually validates data, new bugs might come out from under the rug. - postgres doesn't store nul bytes and a few other non-printables in text fields, even when quoted. bytea fields were used when storing binary data (photo.data, attach.data). A new dbescbin() function was added to handle this transparently. - postgres does not support LIMIT #,# syntax. All databases support LIMIT # OFFSET # syntax. Statements were updated to be standard. These changes require corresponding changes in the coding standards. Please review those before adding any code going forward. Still on my TODO list: - remove quotes from non-reserved identifiers and make reserved identifiers use dba func for quoting - Rewrite search queries for better results (both MySQL and Postgres)
2014-11-13 20:21:58 +00:00
WHERE abook_dob > %s + interval %s and abook_dob < %s + interval %s",
2021-12-03 03:01:39 +00:00
db_utcnow(),
db_quoteinterval('7 day'),
db_utcnow(),
db_quoteinterval('14 day')
);
if ($r) {
foreach ($r as $rr) {
if (! perm_is_allowed($rr['abook_channel'], $rr['xchan_hash'], 'send_stream')) {
continue;
}
$ev = [
'uid' => $rr['abook_channel'],
'account' => $rr['abook_account'],
'event_xchan' => $rr['xchan_hash'],
'dtstart' => datetime_convert('UTC', 'UTC', $rr['abook_dob']),
'dtend' => datetime_convert('UTC', 'UTC', $rr['abook_dob'] . ' + 1 day '),
2022-01-25 23:20:02 +00:00
'adjust' => intval(Features::enabled($rr['abook_channel'], 'smart_birthdays')),
2021-12-03 03:01:39 +00:00
'summary' => sprintf(t('%1$s\'s birthday'), $rr['xchan_name']),
'description' => sprintf(t('Happy Birthday %1$s'), '[zrl=' . $rr['xchan_url'] . ']' . $rr['xchan_name'] . '[/zrl]'),
'etype' => 'birthday',
];
$z = event_store_event($ev);
if ($z) {
2022-11-20 06:44:13 +00:00
event_store_item($ev, $z);
2021-12-03 03:01:39 +00:00
q(
"update abook set abook_dob = '%s' where abook_id = %d",
dbesc(intval($rr['abook_dob']) + 1 . substr($rr['abook_dob'], 4)),
intval($rr['abook_id'])
);
}
}
}
2014-10-18 20:17:49 +00:00
}