1<?php
2/**
3 * DokuWiki Plugin booking (Helper Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Andreas Gohr <dokuwiki@cosmocode.de>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) {
11    die();
12}
13
14class helper_plugin_booking extends DokuWiki_Plugin
15{
16    const E_NOLENGTH = 1;
17    const E_OVERLAP = 2;
18
19
20    /**
21     * Get the filename where the booking data is stored for this resource
22     *
23     * @param string $id
24     * @return string
25     */
26    public function getFile($id)
27    {
28        global $conf;
29        $id = cleanID($id);
30        $file = $conf['metadir'] . '/' . utf8_encodeFN(str_replace(':', '/', "$id.booking"));
31        return $file;
32    }
33
34    /**
35     * Get all existing bookings for a given resource
36     *
37     * @param string $id Page ID of the resource
38     * @param int $from List bookings from this timestamp onwards (0 for all)
39     * @param int $to List bookings up to this timestamp (0 for all)
40     * @return array
41     */
42    public function getBookings($id, $from = 0, $to = 0)
43    {
44        $file = $this->getFile($id);
45        if (!file_exists($file)) return [];
46
47        $fh = fopen($file, 'r');
48        if (!$fh) return [];
49
50        $bookings = [];
51
52        while (($line = fgets($fh, 4096)) !== false) {
53            $line = trim($line);
54            if ($line === '') continue;
55            list($start, $end, $user) = explode("\t", $line, 3);
56            if ($to) {
57                // list all overlapping bookings
58                if ($start > $to) continue;
59                if ($end <= $from) continue;
60            } else {
61                // list all bookings that have not been ended at $from
62                if ($end < $from) continue;
63            }
64
65            // we use the start time as index, for sorting
66            $bookings[$start] = [
67                'start' => $start,
68                'end' => $end,
69                'user' => $user];
70        }
71
72        fclose($fh);
73
74        ksort($bookings);
75        return $bookings;
76    }
77
78    /**
79     * Parses simple time length strings to seconds
80     *
81     * @param string $time
82     * @return int Returns 0 when the time could not be parsed
83     */
84    public function parseTime($time)
85    {
86        $time = trim($time);
87
88        if (preg_match('/([\d\.,]+)([dhm])/i', $time, $m)) {
89            $val = floatval(str_replace(',', '.', $m[1]));
90            $unit = strtolower($m[2]);
91
92            // convert to seconds
93            if ($unit === 'd') {
94                $val = $val * 60 * 60 * 8;
95            } elseif ($unit === 'h') {
96                $val = $val * 60 * 60;
97            } else {
98                $val = $val * 60;
99            }
100
101            return (int)$val;
102        }
103
104        return 0;
105    }
106
107    /**
108     * Adds a booking
109     *
110     * @param string $id resource to book
111     * @param string $begin strtotime compatible start datetime
112     * @param string $length length of booking
113     * @param string $user user doing the booking
114     * @throws Exception when a booking can't be added
115     */
116    public function addBooking($id, $begin, $length, $user)
117    {
118        $file = $this->getFile($id);
119        io_makeFileDir($file);
120
121        $start = strtotime($begin);
122        $end = $start + $this->parseTime($length);
123        if ($start == $end) throw new \Exception('No valid length specified', self::E_NOLENGTH);
124
125        $conflicts = $this->getBookings($id, $start, $end);
126        if ($conflicts) throw new \Exception('Existing booking overlaps', self::E_OVERLAP);
127
128        $line = "$start\t$end\t$user\n";
129        file_put_contents($file, $line, FILE_APPEND);
130    }
131
132    /**
133     * Cancel a booking
134     *
135     * The booking line is replaced by spaces in the file
136     *
137     * @param string $id The booking resource
138     * @param string|int $at The start time of the booking to cancel. Use int for timestamp
139     * @param string|null $user Only cancel if the user matches, null for no check
140     * @return bool Was any booking canceled?
141     */
142    public function cancelBooking($id, $at, $user=null)
143    {
144        $file = $this->getFile($id);
145        if (!file_exists($file)) return false;
146
147        $fh = fopen($file, 'r+');
148        if (!$fh) return false;
149
150        // we support ints and time strings
151        if (!is_int($at)) {
152            $at = strtotime($at);
153        }
154
155        while (($line = fgets($fh, 4096)) !== false) {
156            list($start, ,$booker) = explode("\t", $line, 3);
157            if ($start != $at) continue;
158            if ($user && $user != trim($booker)) continue;
159
160            $len = strlen($line); // length of line (includes newline)
161            fseek($fh, -1 * $len, SEEK_CUR); // jump back to beginning of line
162            fwrite($fh, str_pad('', $len - 1)); // write spaces instead (keep new line)
163            return true;
164        }
165        fclose($fh);
166
167        return false;
168    }
169}
170
171