mirror of
https://gitlab.com/davical-project/davical.git
synced 2026-02-05 02:03:54 +00:00
212 lines
7.2 KiB
PL/PgSQL
212 lines
7.2 KiB
PL/PgSQL
/**
|
|
* PostgreSQL Functions for CalDAV handling
|
|
*
|
|
* @package rscds
|
|
* @subpackage database
|
|
* @author Andrew McMillan <andrew@catalyst.net.nz>
|
|
* @copyright Catalyst IT Ltd
|
|
* @license http://gnu.org/copyleft/gpl.html GNU GPL v2
|
|
*/
|
|
|
|
CREATE or REPLACE FUNCTION apply_month_byday( TIMESTAMP WITH TIME ZONE, TEXT ) RETURNS TIMESTAMP WITH TIME ZONE AS '
|
|
DECLARE
|
|
in_time ALIAS FOR $1;
|
|
byday ALIAS FOR $2;
|
|
weeks INT;
|
|
dow INT;
|
|
temp_txt TEXT;
|
|
dd INT;
|
|
mm INT;
|
|
yy INT;
|
|
our_dow INT;
|
|
our_answer TIMESTAMP WITH TIME ZONE;
|
|
BEGIN
|
|
dow := position(substring( byday from ''..$'') in ''SUMOTUWETHFRSA'') / 2;
|
|
temp_txt := substring(byday from ''([0-9]+)'');
|
|
weeks := temp_txt::int;
|
|
|
|
-- RAISE NOTICE ''DOW: %, Weeks: %(%s)'', dow, weeks, temp_txt;
|
|
|
|
IF substring(byday for 1) = ''-'' THEN
|
|
-- Last XX of month, or possibly second-to-last, but unlikely
|
|
mm := extract( ''month'' from in_time);
|
|
yy := extract( ''year'' from in_time);
|
|
|
|
-- Start with the last day of the month
|
|
our_answer := (yy::text || ''-'' || (mm+1)::text || ''-01'')::timestamp - ''1 day''::interval;
|
|
dd := extract( ''dow'' from our_answer);
|
|
dd := dd - dow;
|
|
IF dd < 0 THEN
|
|
dd := dd + 7;
|
|
END IF;
|
|
|
|
-- Having calculated the right day of the month, we now apply that back to in_time
|
|
-- which contains the otherwise-unobtainable timezone detail (and the time)
|
|
our_answer = our_answer - (dd::text || ''days'')::interval;
|
|
dd := extract( ''day'' from our_answer) - extract( ''day'' from in_time);
|
|
our_answer := in_time + (dd::text || ''days'')::interval;
|
|
|
|
IF weeks > 1 THEN
|
|
weeks := weeks - 1;
|
|
our_answer := our_answer - (weeks::text || ''weeks'')::interval;
|
|
END IF;
|
|
|
|
ELSE
|
|
|
|
-- Shift our date to the correct day of week..
|
|
our_dow := extract( ''dow'' from in_time);
|
|
our_dow := our_dow - dow;
|
|
dd := extract( ''day'' from in_time);
|
|
IF our_dow >= dd THEN
|
|
our_dow := our_dow - 7;
|
|
END IF;
|
|
our_answer := in_time - (our_dow::text || ''days'')::interval;
|
|
dd = extract( ''day'' from our_answer);
|
|
|
|
-- Shift the date to the correct week...
|
|
dd := weeks - ((dd+6) / 7);
|
|
IF dd != 0 THEN
|
|
our_answer := our_answer + ((dd::text || ''weeks'')::interval);
|
|
END IF;
|
|
|
|
END IF;
|
|
|
|
RETURN our_answer;
|
|
|
|
END;
|
|
' LANGUAGE 'plpgsql' IMMUTABLE STRICT;
|
|
|
|
|
|
CREATE or REPLACE FUNCTION calculate_later_timestamp( TIMESTAMP WITH TIME ZONE, TIMESTAMP WITH TIME ZONE, TEXT ) RETURNS TIMESTAMP WITH TIME ZONE AS '
|
|
DECLARE
|
|
earliest ALIAS FOR $1;
|
|
basedate ALIAS FOR $2;
|
|
repeatrule ALIAS FOR $3;
|
|
frequency TEXT;
|
|
temp_txt TEXT;
|
|
length INT;
|
|
count INT;
|
|
byday TEXT;
|
|
bymonthday INT;
|
|
basediff INTERVAL;
|
|
past_repeats INT8;
|
|
units TEXT;
|
|
dow TEXT;
|
|
our_answer TIMESTAMP WITH TIME ZONE;
|
|
loopcount INT;
|
|
BEGIN
|
|
temp_txt := substring(repeatrule from ''UNTIL=([0-9TZ]+)(;|$)'');
|
|
IF temp_txt::timestamp with time zone < earliest THEN
|
|
RETURN NULL;
|
|
END IF;
|
|
|
|
frequency := substring(repeatrule from ''FREQ=([A-Z]+)(;|$)'');
|
|
temp_txt := substring(repeatrule from ''INTERVAL=([0-9]+)(;|$)'');
|
|
length := temp_txt::int;
|
|
basediff := earliest - basedate;
|
|
|
|
-- RAISE NOTICE ''Frequency: %, Length: %(%), Basediff: %'', frequency, length, temp_txt, basediff;
|
|
|
|
-- Calculate the number of past periods between our base date and our earliest date
|
|
IF frequency = ''WEEKLY'' OR frequency = ''DAILY'' THEN
|
|
past_repeats := extract(''epoch'' from basediff)::INT8 / 86400;
|
|
-- RAISE NOTICE ''Days: %'', past_repeats;
|
|
IF frequency = ''WEEKLY'' THEN
|
|
past_repeats := past_repeats / 7;
|
|
END IF;
|
|
ELSE
|
|
past_repeats = extract( ''years'' from basediff );
|
|
IF frequency = ''MONTHLY'' THEN
|
|
past_repeats = (past_repeats *12) + extract( ''months'' from basediff );
|
|
END IF;
|
|
END IF;
|
|
past_repeats = (past_repeats / length) + 1;
|
|
|
|
-- Check that we have not exceeded the COUNT= limit
|
|
temp_txt := substring(repeatrule from ''COUNT=([0-9]+)(;|$)'');
|
|
count := temp_txt::int;
|
|
-- RAISE NOTICE ''Periods: %, Count: %(%)'', past_repeats, count, temp_txt;
|
|
IF ( count <= past_repeats ) THEN
|
|
RETURN NULL;
|
|
END IF;
|
|
|
|
temp_txt := substring(repeatrule from ''BYSETPOS=([0-9-]+)(;|$)'');
|
|
byday := substring(repeatrule from ''BYDAY=([0-9A-Z,]+-)(;|$)'');
|
|
IF byday IS NOT NULL AND frequency = ''MONTHLY'' THEN
|
|
-- Since this could move the date around a month we go back one
|
|
-- period just to be extra sure.
|
|
past_repeats = past_repeats - 1;
|
|
|
|
IF temp_txt IS NOT NULL THEN
|
|
-- Crudely hack the BYSETPOS onto the front of BYDAY. While this
|
|
-- is not as per rfc2445, RRULE syntax is so complex and overblown
|
|
-- that nobody correctly uses comma-separated BYDAY or BYSETPOS, and
|
|
-- certainly not within a MONTHLY RRULE.
|
|
byday := temp_txt || byday;
|
|
END IF;
|
|
END IF;
|
|
|
|
past_repeats = past_repeats * length;
|
|
|
|
units := CASE
|
|
WHEN frequency = ''DAILY'' THEN ''days''
|
|
WHEN frequency = ''WEEKLY'' THEN ''weeks''
|
|
WHEN frequency = ''MONTHLY'' THEN ''months''
|
|
WHEN frequency = ''YEARLY'' THEN ''years''
|
|
END;
|
|
|
|
temp_txt := substring(repeatrule from ''BYMONTHDAY=([0-9,]+)(;|$)'');
|
|
bymonthday := temp_txt::int;
|
|
|
|
-- With all of the above calculation, this date should be close to (but less than)
|
|
-- the target, and we should only loop once or twice.
|
|
our_answer := basedate + (past_repeats::text || units)::interval;
|
|
|
|
loopcount := 1000; -- Not really needed, but stops an infinite loop if there is a bug!
|
|
LOOP
|
|
-- RAISE NOTICE ''Testing date: %'', our_answer;
|
|
IF frequency = ''WEEKLY'' THEN
|
|
-- Weekly repeats are only on specific days
|
|
-- I think this is not really right, since a WEEKLY on MO,WE,FR should
|
|
-- occur three times each week and this will only be once a week.
|
|
dow = substring( to_char( our_answer, ''DY'' ) for 2);
|
|
CONTINUE WHEN position( dow in byday ) = 0;
|
|
ELSIF frequency = ''MONTHLY'' AND byday IS NOT NULL THEN
|
|
-- This works fine, except that maybe there are multiple BYDAY
|
|
-- components. e.g. 1TU,3TU might be 1st & 3rd tuesdays.
|
|
our_answer := apply_month_byday( our_answer, byday );
|
|
ELSIF bymonthday IS NOT NULL AND frequency = ''MONTHLY'' AND bymonthday < 1 THEN
|
|
-- We do not deal with this situation at present
|
|
RAISE NOTICE ''The case of negative BYMONTHDAY is not handled yet.'';
|
|
END IF;
|
|
|
|
EXIT WHEN our_answer >= earliest;
|
|
|
|
loopcount := loopcount - 1;
|
|
IF loopcount < 0 THEN
|
|
RAISE EXCEPTION ''Could not cope with dates after % using % from %'', earliest, repeatrule, basedate;
|
|
RETURN NULL;
|
|
END IF;
|
|
|
|
-- Increment for our next time through the loop...
|
|
our_answer := our_answer + (length::text || units)::interval;
|
|
END LOOP;
|
|
|
|
RETURN our_answer;
|
|
|
|
END;
|
|
' LANGUAGE 'plpgsql' IMMUTABLE STRICT;
|
|
|
|
CREATE or REPLACE FUNCTION get_permissions( INT, INT ) RETURNS TEXT AS '
|
|
SELECT rt.confers FROM relationship r1
|
|
JOIN relationship_type rt USING ( rt_id )
|
|
WHERE NOT rt.rt_isgroup
|
|
AND r1.from_user = $1 AND r1.to_user = $2
|
|
UNION
|
|
SELECT rt.confers FROM relationship r1
|
|
JOIN relationship r2 ON ( r1.to_user = r2.from_user)
|
|
JOIN relationship_type rt ON (r2.rt_id = rt.rt_id)
|
|
WHERE rt.rt_isgroup
|
|
AND r1.from_user = $1 AND r2.to_user = $2;
|
|
' LANGUAGE 'sql' IMMUTABLE STRICT;
|