Overview

Request 2758 (accepted)

Check in 0.4.4

Submit package Kolab:Winterfell / iRony to package Kolab:16 / iRony

iRony.spec Changed
x
 
1
@@ -37,7 +37,7 @@
2
 %global _ap_sysconfdir %{_sysconfdir}/%{httpd_name}
3
 
4
 Name:           iRony
5
-Version:        0.4.3
6
+Version:        0.4.4
7
 Release:       1%{?dist}
8
 Summary:        DAV for Kolab Groupware
9
 
10
@@ -185,6 +185,9 @@
11
 %attr(0770,%{httpd_user},%{httpd_group}) %{_localstatedir}/log/%{name}
12
 
13
 %changelog
14
+* Fri Oct 16 2020 Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> - 0.4.4-1
15
+- Release of version 0.4.4
16
+
17
 * Thu Jan 16 2020 Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> - 0.4.3-1
18
 - Release of version 0.4.3
19
 
20
debian.changelog Changed
11
 
1
@@ -1,3 +1,9 @@
2
+irony (0.4.4-1~kolab1) unstable; urgency=low
3
+
4
+  * Release of version 0.4.4
5
+
6
+ -- Jeroen van Meeuwen <vanmeeuwen@kolabsys.com>  Fri, 16 Oct 2020 12:12:13 +0100
7
+
8
 irony (0.4.3-1~kolab2) unstable; urgency=low
9
 
10
   * Release of version 0.4.3
11
iRony-0.4.3.tar.gz/lib/FileAPI/api/autocomplete.php Deleted
59
 
1
@@ -1,57 +0,0 @@
2
-<?php
3
-/*
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2018, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-class file_api_autocomplete extends file_api_common
27
-{
28
-    /**
29
-     * Request handler
30
-     */
31
-    public function handle()
32
-    {
33
-        parent::handle();
34
-
35
-        if (!isset($this->args['search']) || $this->args['search'] === '') {
36
-            throw new Exception("Missing search keyword", file_api_core::ERROR_CODE);
37
-        }
38
-
39
-        if (isset($this->args['folder']) && $this->args['folder'] !== '') {
40
-            list($driver, $path) = $this->api->get_driver($this->args['folder']);
41
-        }
42
-        else {
43
-            $driver = $this->api->get_backend();
44
-        }
45
-
46
-        if (!empty($this->args['mode'])) {
47
-            $mode = 0;
48
-            $mode += stripos($this->args['mode'], 'user') !== false ? file_storage::SEARCH_USER : 0;
49
-            $mode += stripos($this->args['mode'], 'group') !== false ? file_storage::SEARCH_GROUP : 0;
50
-        }
51
-
52
-        if (empty($mode)) {
53
-            $mode = file_storage::SEARCH_USER;
54
-        }
55
-
56
-        return $driver->autocomplete($this->args['search'], $mode);
57
-    }
58
-}
59
iRony-0.4.3.tar.gz/lib/FileAPI/api/document.php Deleted
347
 
1
@@ -1,345 +0,0 @@
2
-<?php
3
-/**
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2015, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-class file_api_document extends file_api_common
27
-{
28
-    /**
29
-     * Request handler
30
-     */
31
-    public function handle()
32
-    {
33
-        $method     = $_SERVER['REQUEST_METHOD'];
34
-        $this->args = $_GET;
35
-
36
-        if ($method == 'POST' && !empty($_SERVER['HTTP_X_HTTP_METHOD'])) {
37
-            $method = $_SERVER['HTTP_X_HTTP_METHOD'];
38
-        }
39
-
40
-        // Invitation notifications
41
-        if ($this->args['method'] == 'invitations') {
42
-            return $this->invitations();
43
-        }
44
-
45
-        // Sessions list
46
-        if ($this->args['method'] == 'sessions') {
47
-            return $this->sessions();
48
-        }
49
-
50
-        // Session and invitations management
51
-        if (strpos($this->args['method'], 'document_') === 0) {
52
-            if ($_SERVER['REQUEST_METHOD'] == 'POST') {
53
-                $post = file_get_contents('php://input');
54
-                $this->args += (array) json_decode($post, true);
55
-                unset($post);
56
-            }
57
-
58
-            if (empty($this->args['id'])) {
59
-                throw new Exception("Missing document ID.", file_api_core::ERROR_CODE);
60
-            }
61
-
62
-            switch ($this->args['method']) {
63
-                case 'document_delete':
64
-                case 'document_invite':
65
-                case 'document_request':
66
-                case 'document_decline':
67
-                case 'document_accept':
68
-                case 'document_cancel':
69
-                case 'document_info':
70
-                    return $this->{$this->args['method']}($this->args['id']);
71
-            }
72
-        }
73
-        // Document content actions for Manticore
74
-        else if ($method == 'PUT' || $method == 'GET') {
75
-            if (empty($this->args['id'])) {
76
-                throw new Exception("Missing document ID.", file_api_core::ERROR_CODE);
77
-            }
78
-
79
-            return $this->{'document_' . strtolower($method)}($this->args['id']);
80
-        }
81
-
82
-        throw new Exception("Unknown method", file_api_core::ERROR_INVALID);
83
-    }
84
-
85
-    /**
86
-     * Get file path from manticore session identifier
87
-     */
88
-    protected function get_file_path($id)
89
-    {
90
-        $document = new file_document($this->api);
91
-
92
-        $file = $document->session_file($id);
93
-
94
-        return $file['file'];
95
-    }
96
-
97
-    /**
98
-     * Get invitations list
99
-     */
100
-    protected function invitations()
101
-    {
102
-        $timestamp = new DateTime('now', new DateTimeZone('UTC'));
103
-        $timestamp = $timestamp->format('U');
104
-
105
-        // Initial tracking request, return just the current timestamp
106
-        if ($this->args['timestamp'] == -1) {
107
-            return array('timestamp' => $timestamp);
108
-            // @TODO: in this mode we should likely return all invitations
109
-            // that require user action, otherwise we may skip some unintentionally
110
-        }
111
-
112
-        $document = new file_document($this->api);
113
-        $filter   = array();
114
-
115
-        if ($this->args['timestamp']) {
116
-            $filter['timestamp'] = $this->args['timestamp'];
117
-        }
118
-
119
-        $list = $document->invitations_list($filter);
120
-
121
-        return array(
122
-            'list'      => $list,
123
-            'timestamp' => $timestamp,
124
-        );
125
-    }
126
-
127
-    /**
128
-     * Get sessions list
129
-     */
130
-    protected function sessions()
131
-    {
132
-        $document = new file_document($this->api);
133
-
134
-        $params = array(
135
-            'reverse' => rcube_utils::get_boolean((string) $this->args['reverse']),
136
-        );
137
-
138
-        if (!empty($this->args['sort'])) {
139
-            $params['sort'] = strtolower($this->args['sort']);
140
-        }
141
-
142
-        return $document->sessions_list($params);
143
-    }
144
-
145
-    /**
146
-     * Close (delete) manticore session
147
-     */
148
-    protected function document_delete($id)
149
-    {
150
-        $document = file_document::get_handler($this->api, $id);
151
-
152
-        if (!$document->session_delete($id)) {
153
-            throw new Exception("Failed deleting the document session.", file_api_core::ERROR_CODE);
154
-        }
155
-    }
156
-
157
-    /**
158
-     * Invite/add a session participant(s)
159
-     */
160
-    protected function document_invite($id)
161
-    {
162
-        $document = file_document::get_handler($this->api, $id);
163
-        $users    = $this->args['users'];
164
-        $comment  = $this->args['comment'];
165
-
166
-        if (empty($users)) {
167
-            throw new Exception("Invalid arguments.", file_api_core::ERROR_CODE);
168
-        }
169
-
170
-        foreach ((array) $users as $user) {
171
-            if (!empty($user['user'])) {
172
-                $document->invitation_create($id, $user['user'], file_document::STATUS_INVITED, $comment, $user['name']);
173
-
174
-                $result[] = array(
175
-                    'session_id' => $id,
176
-                    'user'       => $user['user'],
177
-                    'user_name'  => $user['name'],
178
-                    'status'     => file_document::STATUS_INVITED,
179
-                );
180
-            }
181
-        }
182
-
183
-        return array(
184
-            'list' => $result,
185
-        );
186
-    }
187
-
188
-    /**
189
-     * Request an invitation to a session
190
-     */
191
-    protected function document_request($id)
192
-    {
193
-        $document = file_document::get_handler($this->api, $id);
194
-        $document->invitation_create($id, null, file_document::STATUS_REQUESTED, $this->args['comment']);
195
-    }
196
-
197
-    /**
198
-     * Decline an invitation to a session
199
-     */
200
-    protected function document_decline($id)
201
-    {
202
-        $document = file_document::get_handler($this->api, $id);
203
-        $document->invitation_update($id, $this->args['user'], file_document::STATUS_DECLINED, $this->args['comment']);
204
-    }
205
-
206
-    /**
207
-     * Accept an invitation to a session
208
-     */
209
-    protected function document_accept($id)
210
-    {
211
-        $document = file_document::get_handler($this->api, $id);
212
-        $document->invitation_update($id, $this->args['user'], file_document::STATUS_ACCEPTED, $this->args['comment']);
213
-    }
214
-
215
-    /**
216
-     * Remove a session participant(s) - cancel invitations
217
-     */
218
-    protected function document_cancel($id)
219
-    {
220
-        $document = file_document::get_handler($this->api, $id);
221
-        $users    = $this->args['users'];
222
-
223
-        if (empty($users)) {
224
-            throw new Exception("Invalid arguments.", file_api_core::ERROR_CODE);
225
-        }
226
-
227
-        foreach ((array) $users as $user) {
228
-            $document->invitation_delete($id, $user);
229
-            $result[] = $user;
230
-        }
231
-
232
-        return array(
233
-            'list' => $result,
234
-        );
235
-    }
236
-
237
-    /**
238
-     * Return document informations
239
-     */
240
-    protected function document_info($id, $extended = true)
241
-    {
242
-        $document = file_document::get_handler($this->api, $id);
243
-        $file     = $document->session_file($id);
244
-        $rcube    = rcube::get_instance();
245
-
246
-        try {
247
-            list($driver, $path) = $this->api->get_driver($file['file']);
248
-            $result = $driver->file_info($path);
249
-        }
250
-        catch (Exception $e) {
251
-            // invited users may have no permission,
252
-            // use file data from the session
253
-            $result = array(
254
-                'size'     => $file['size'],
255
-                'name'     => $file['name'],
256
-                'modified' => $file['modified'],
257
-                'type'     => $file['type'],
258
-            );
259
-        }
260
-
261
-        if ($extended) {
262
-            $session = $document->session_info($id);
263
-
264
-            $result['owner']      = $session['owner'];
265
-            $result['owner_name'] = $session['owner_name'];
266
-            $result['user']       = $rcube->user->get_username();
267
-            $result['readonly']   = !empty($session['readonly']);
268
-            $result['origin']     = $session['origin'];
269
-
270
-            if ($result['owner'] == $result['user']) {
271
-                $result['user_name'] = $result['owner_name'];
272
-            }
273
-            else {
274
-                $result['user_name'] = $this->api->resolve_user($result['user']) ?: '';
275
-            }
276
-        }
277
-
278
-        return $result;
279
-    }
280
-
281
-    /**
282
-     * Update document file content
283
-     */
284
-    protected function document_put($id)
285
-    {
286
-        $file = $this->get_file_path($id);
287
-        list($driver, $path) = $this->api->get_driver($file);
288
-
289
-        $length   = rcube_utils::request_header('Content-Length');
290
-        $tmp_dir  = unslashify($this->api->config->get('temp_dir'));
291
-        $tmp_path = tempnam($tmp_dir, 'chwalaUpload');
292
-
293
-        // Create stream to copy input into a temp file
294
-        $input    = fopen('php://input', 'r');
295
-        $tmp_file = fopen($tmp_path, 'w');
296
-
297
-        if (!$input || !$tmp_file) {
298
-            throw new Exception("Failed opening input or temp file stream.", file_api_core::ERROR_CODE);
299
-        }
300
-
301
-        // Create temp file from the input
302
-        $copied = stream_copy_to_stream($input, $tmp_file);
303
-
304
-        fclose($input);
305
-        fclose($tmp_file);
306
-
307
-        if ($copied < $length) {
308
-            throw new Exception("Failed writing to temp file.", file_api_core::ERROR_CODE);
309
-        }
310
-
311
-        $file_data = array(
312
-            'path' => $tmp_path,
313
-            'type' => rcube_mime::file_content_type($tmp_path, $file),
314
-        );
315
-
316
-        $driver->file_update($path, $file_data);
317
-
318
-        // remove the temp file
319
-        unlink($tmp_path);
320
-
321
-        // Update the file metadata in session
322
-        $file_data = $driver->file_info($file);
323
-        $document  = file_document::get_handler($this->api, $this->args['id']);
324
-        $document->session_update($this->args['id'], $file_data);
325
-    }
326
-
327
-    /**
328
-     * Return document file content
329
-     */
330
-    protected function document_get($id)
331
-    {
332
-        $file = $this->get_file_path($id);
333
-        list($driver, $path) = $this->api->get_driver($file);
334
-
335
-        try {
336
-            $params = array('force-type' => 'application/vnd.oasis.opendocument.text');
337
-
338
-            $driver->file_get($path, $params);
339
-        }
340
-        catch (Exception $e) {
341
-            header("HTTP/1.0 " . file_api_core::ERROR_CODE . " " . $e->getMessage());
342
-        }
343
-
344
-        $this->api->output_send();
345
-    }
346
-}
347
iRony-0.4.3.tar.gz/lib/FileAPI/api/folder_info.php Deleted
63
 
1
@@ -1,61 +0,0 @@
2
-<?php
3
-/**
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2015, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-class file_api_folder_info extends file_api_common
27
-{
28
-    /**
29
-     * Request handler
30
-     */
31
-    public function handle()
32
-    {
33
-        parent::handle();
34
-
35
-        if (!isset($this->args['folder']) || $this->args['folder'] === '') {
36
-            throw new Exception("Missing folder name", file_api_core::ERROR_CODE);
37
-        }
38
-
39
-        $result = array(
40
-            'folder' => $this->args['folder'],
41
-        );
42
-
43
-        if (!empty($this->args['rights']) && rcube_utils::get_boolean((string) $this->args['rights'])) {
44
-            $result['rights'] = $this->folder_rights($this->args['folder']);
45
-        }
46
-
47
-        if (!empty($this->args['sessions']) && rcube_utils::get_boolean((string) $this->args['sessions'])) {
48
-             $result['sessions'] = $this->folder_sessions($this->args['folder']);
49
-        }
50
-
51
-        return $result;
52
-    }
53
-
54
-    /**
55
-     * Get editing sessions
56
-     */
57
-    protected function folder_sessions($folder)
58
-    {
59
-        $manticore = new file_manticore($this->api);
60
-        return $manticore->session_find($folder);
61
-    }
62
-}
63
iRony-0.4.3.tar.gz/lib/FileAPI/api/folder_subscribe.php Deleted
50
 
1
@@ -1,48 +0,0 @@
2
-<?php
3
-/*
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2015, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-class file_api_folder_subscribe extends file_api_common
27
-{
28
-    /**
29
-     * Request handler
30
-     */
31
-    public function handle()
32
-    {
33
-        parent::handle();
34
-
35
-        if (!isset($this->args['folder']) || $this->args['folder'] === '') {
36
-            throw new Exception("Missing folder name", file_api_core::ERROR_CODE);
37
-        }
38
-
39
-        list($driver, $path) = $this->api->get_driver($this->args['folder']);
40
-
41
-        // subscribe mount point?
42
-        if ($driver->title() === $this->args['folder']) {
43
-            throw new Exception("Mount point is subscribed by default", file_api_core::ERROR_CODE);
44
-        }
45
-
46
-        // subscribe folder...
47
-        $driver->folder_subscribe($path);
48
-    }
49
-}
50
iRony-0.4.3.tar.gz/lib/FileAPI/api/folder_unsubscribe.php Deleted
50
 
1
@@ -1,48 +0,0 @@
2
-<?php
3
-/*
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2015, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-class file_api_folder_unsubscribe extends file_api_common
27
-{
28
-    /**
29
-     * Request handler
30
-     */
31
-    public function handle()
32
-    {
33
-        parent::handle();
34
-
35
-        if (!isset($this->args['folder']) || $this->args['folder'] === '') {
36
-            throw new Exception("Missing folder name", file_api_core::ERROR_CODE);
37
-        }
38
-
39
-        list($driver, $path) = $this->api->get_driver($this->args['folder']);
40
-
41
-        // subscribe mount point?
42
-        if ($driver->title() === $this->args['folder']) {
43
-            throw new Exception("Mount point cannot be unsubscribed", file_api_core::ERROR_CODE);
44
-        }
45
-
46
-        // unsubscribe folder...
47
-        $driver->folder_unsubscribe($path);
48
-    }
49
-}
50
iRony-0.4.3.tar.gz/lib/FileAPI/api/sharing.php Deleted
86
 
1
@@ -1,84 +0,0 @@
2
-<?php
3
-/*
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2018, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-class file_api_sharing extends file_api_common
27
-{
28
-    /**
29
-     * Request handler
30
-     */
31
-    public function handle()
32
-    {
33
-        parent::handle();
34
-
35
-        if (!isset($this->args['folder']) || $this->args['folder'] === '') {
36
-            throw new Exception("Missing folder name", file_api_core::ERROR_CODE);
37
-        }
38
-
39
-        list($driver, $path) = $this->api->get_driver($this->args['folder']);
40
-
41
-        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
42
-            // Required arguments:
43
-            // - action: 'submit', 'update', 'delete'
44
-            // - mode: depends on the storage driver
45
-            return $driver->sharing($path, file_storage::SHARING_MODE_UPDATE, $this->args);
46
-        }
47
-
48
-        $form = $driver->sharing($path, file_storage::SHARING_MODE_FORM);
49
-
50
-        if (empty($form)) {
51
-            throw new Exception("Sharing not supported", file_api_core::ERROR_UNSUPPORTED);
52
-        }
53
-
54
-        $rights = $driver->sharing($path, file_storage::SHARING_MODE_RIGHTS);
55
-
56
-        $this->localize_form_data($form);
57
-
58
-        $result = array(
59
-            'form'   => $form,
60
-            'rights' => (array) $rights,
61
-        );
62
-
63
-        return $result;
64
-    }
65
-
66
-    /**
67
-     * Recurrent function to localize form data entries
68
-     */
69
-    protected function localize_form_data(&$data, $key = null, $self = null)
70
-    {
71
-        if (!$self) {
72
-            $self = $this;
73
-        }
74
-
75
-        if (in_array($key, array('title', 'placeholder', 'label', 'list_column_label'))) {
76
-            $data = $self->api->translate($data);
77
-        }
78
-        else if ($key == 'options') {
79
-            $data = array_map(array($self->api, 'translate'), $data);
80
-        }
81
-        else if (is_array($data)) {
82
-            array_walk($data, array($self, 'localize_form_data'), $self);
83
-        }
84
-    }
85
-}
86
iRony-0.4.3.tar.gz/lib/FileAPI/drivers/kolab/kolab_file_autocomplete.php Deleted
223
 
1
@@ -1,221 +0,0 @@
2
-<?php
3
-/*
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2018, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-class kolab_file_autocomplete
27
-{
28
-    private $rc;
29
-    private $ldap;
30
-
31
-    /**
32
-     * Class constructor
33
-     */
34
-    public function __construct()
35
-    {
36
-        $this->rc = rcube::get_instance();
37
-    }
38
-
39
-    /**
40
-     * Search users/groups
41
-     */
42
-    public function search($search, $with_groups = false)
43
-    {
44
-        if (!$this->init_ldap()) {
45
-            return false;
46
-        }
47
-
48
-        $max  = (int) $this->rc->config->get('autocomplete_max', 15);
49
-        $mode = (int) $this->rc->config->get('addressbook_search_mode');
50
-        $me   = $this->rc->get_user_name();
51
-
52
-        $this->ldap->set_pagesize($max);
53
-
54
-        $result = $this->ldap->search('*', $search, $mode);
55
-        $users  = array();
56
-        $index  = array();
57
-
58
-        foreach ($result->records as $record) {
59
-            $user = $record['uid'];
60
-
61
-            if (is_array($user)) {
62
-                $user = array_filter($user);
63
-                $user = $user[0];
64
-            }
65
-
66
-            if (in_array($me, rcube_addressbook::get_col_values('email', $record, true))) {
67
-                continue;
68
-            }
69
-
70
-            if ($user) {
71
-                $display = rcube_addressbook::compose_search_name($record);
72
-                $user    = array('name' => $user, 'display' => $display);
73
-                $users[] = $user;
74
-                $index[] = $display ?: $user['name'];
75
-            }
76
-        }
77
-
78
-        $group_support = $this->rc->config->get('fileapi_groups');
79
-        $group_prefix  = $this->rc->config->get('fileapi_group_prefix');
80
-        $group_field   = $this->rc->config->get('fileapi_group_field', 'name');
81
-
82
-        if ($with_groups && $group_support && $group_field) {
83
-            $result = $this->ldap->list_groups($search, $mode);
84
-
85
-            foreach ($result as $record) {
86
-                $group    = $record['name'];
87
-                $group_id = is_array($record[$group_field]) ? $record[$group_field][0] : $record[$group_field];
88
-
89
-                if ($group) {
90
-                    $users[] = array('name' => ($group_prefix ? $group_prefix : '') . $group_id, 'display' => $group, 'type' => 'group');
91
-                    $index[] = $group;
92
-                }
93
-            }
94
-        }
95
-
96
-        if (count($users)) {
97
-            array_multisort($index, SORT_ASC, SORT_LOCALE_STRING, $users);
98
-        }
99
-
100
-        if (count($users) > $max) {
101
-            $users = array_slice($users, 0, $max);
102
-        }
103
-
104
-        return $users;
105
-    }
106
-
107
-    /**
108
-     * Resolve acl identifier to user/group name
109
-     */
110
-    public function resolve_uid($id, &$title = null)
111
-    {
112
-        $groups      = $this->rc->config->get('acl_groups');
113
-        $prefix      = $this->rc->config->get('acl_group_prefix');
114
-        $group_field = $this->rc->config->get('acl_group_field', 'name');
115
-
116
-        if ($groups && $prefix && strpos($id, $prefix) === 0) {
117
-            $gid = substr($id, strlen($prefix));
118
-
119
-            // Unfortunately this works only if group_field=name,
120
-            // list_groups() allows searching by group name only
121
-            if ($group_field === 'name' && $this->init_ldap()) {
122
-                $result = $this->ldap->list_groups($gid, rcube_addressbook::SEARCH_STRICT);
123
-
124
-                if (count($result) === 1 && ($record = $result[0])) {
125
-                    if ($record[$group_field] === $gid) {
126
-                        $display = $record['name'];
127
-                        if ($display != $gid) {
128
-                            $title = sprintf('%s (%s)', $display, $gid);
129
-                        }
130
-
131
-                        return $display;
132
-                    }
133
-                }
134
-            }
135
-
136
-            return $gid;
137
-        }
138
-
139
-        if ($this->init_ldap()) {
140
-            $this->ldap->set_pagesize('2');
141
-            // Note: 'uid' works here because we overwrite fieldmap in init_ldap() above
142
-            $result = $this->ldap->search('uid', $id, rcube_addressbook::SEARCH_STRICT);
143
-
144
-            if ($result->count === 1 && ($record = $result->first())) {
145
-                if ($record['uid'] === $id) {
146
-                    $title   = rcube_addressbook::compose_search_name($record);
147
-                    $display = rcube_addressbook::compose_list_name($record);
148
-
149
-                    return $display;
150
-                }
151
-            }
152
-        }
153
-
154
-        return $id;
155
-    }
156
-
157
-    /**
158
-     * Initializes autocomplete LDAP backend
159
-     */
160
-    private function init_ldap()
161
-    {
162
-        if ($this->ldap) {
163
-            return $this->ldap->ready;
164
-        }
165
-
166
-        // get LDAP config
167
-        $config = $this->rc->config->get('fileapi_users_source');
168
-
169
-        if (empty($config)) {
170
-            return false;
171
-        }
172
-
173
-        // not an array, use configured ldap_public source
174
-        if (!is_array($config)) {
175
-            $ldap_config = (array) $this->rc->config->get('ldap_public');
176
-            $config      = $ldap_config[$config];
177
-        }
178
-
179
-        $uid_field = $this->rc->config->get('fileapi_users_field', 'mail');
180
-        $filter    = $this->rc->config->get('fileapi_users_filter');
181
-        $debug     = $this->rc->config->get('ldap_debug');
182
-        $domain    = $this->rc->config->mail_domain($_SESSION['imap_host']);
183
-
184
-        if (empty($uid_field) || empty($config)) {
185
-            return false;
186
-        }
187
-
188
-        // get name attribute
189
-        if (!empty($config['fieldmap'])) {
190
-            $name_field = $config['fieldmap']['name'];
191
-        }
192
-        // ... no fieldmap, use the old method
193
-        if (empty($name_field)) {
194
-            $name_field = $config['name_field'];
195
-        }
196
-
197
-        // add UID field to fieldmap, so it will be returned in a record with name
198
-        $config['fieldmap']['name'] = $name_field;
199
-        $config['fieldmap']['uid']  = $uid_field;
200
-
201
-        // search in UID and name fields
202
-        // $name_field can be in a form of <field>:<modifier> (#1490591)
203
-        $name_field = preg_replace('/:.*$/', '', $name_field);
204
-        $search     = array_unique(array($name_field, $uid_field));
205
-
206
-        $config['search_fields']   = $search;
207
-        $config['required_fields'] = array($uid_field);
208
-
209
-        // set search filter
210
-        if ($filter) {
211
-            $config['filter'] = $filter;
212
-        }
213
-
214
-        // disable vlv
215
-        $config['vlv'] = false;
216
-
217
-        // Initialize LDAP connection
218
-        $this->ldap = new rcube_ldap($config, $debug, $domain);
219
-
220
-        return $this->ldap->ready;
221
-    }
222
-}
223
iRony-0.4.3.tar.gz/lib/FileAPI/drivers/webdav Deleted
2
 
1
-(directory)
2
iRony-0.4.3.tar.gz/lib/FileAPI/drivers/webdav/webdav.png Deleted
iRony-0.4.3.tar.gz/lib/FileAPI/drivers/webdav/webdav_file_storage.php Deleted
1117
 
1
@@ -1,1115 +0,0 @@
2
-<?php
3
-/**
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2015, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-// try SabreDAV installed as described in README
27
-if (file_exists(__DIR__ . '/../../ext/SabreDAV/vendor/autoload.php')) {
28
-    require __DIR__ . '/../../ext/SabreDAV/vendor/autoload.php';
29
-}
30
-// fallback to System-installed package
31
-else {
32
-    require 'Sabre/autoload.php';
33
-}
34
-
35
-use Sabre\DAV\Client;
36
-
37
-class webdav_file_storage implements file_storage
38
-{
39
-    /**
40
-     * @var rcube
41
-     */
42
-    protected $rc;
43
-
44
-    /**
45
-     * @var array
46
-     */
47
-    protected $config = array();
48
-
49
-    /**
50
-     * @var string
51
-     */
52
-    protected $title;
53
-
54
-    /**
55
-     * @var Sabre\DAV\Client
56
-     */
57
-    protected $client;
58
-
59
-
60
-    /**
61
-     * Class constructor
62
-     */
63
-    public function __construct()
64
-    {
65
-        $this->rc = rcube::get_instance();
66
-    }
67
-
68
-    /**
69
-     * Authenticates a user
70
-     *
71
-     * @param string $username User name
72
-     * @param string $password User password
73
-     *
74
-     * @param bool True on success, False on failure
75
-     */
76
-    public function authenticate($username, $password)
77
-    {
78
-        $settings = array(
79
-            'baseUri'  => $this->config['baseuri'],
80
-            'userName' => $username,
81
-            'password' => $password,
82
-            'authType' => Client::AUTH_BASIC,
83
-        );
84
-
85
-        $client = new Client($settings);
86
-
87
-        try {
88
-            $client->propfind('', array());
89
-        }
90
-        catch (Exception $e) {
91
-            return false;
92
-        }
93
-
94
-        if ($this->title) {
95
-            $_SESSION[$this->title . '_webdav_user']  = $username;
96
-            $_SESSION[$this->title . '_webdav_pass']  = $this->rc->encrypt($password);
97
-            $this->client = $client;
98
-        }
99
-
100
-        return true;
101
-    }
102
-
103
-    /**
104
-     * Get password and name of authenticated user
105
-     *
106
-     * @return array Authenticated user data
107
-     */
108
-    public function auth_info()
109
-    {
110
-        return array(
111
-            'username' => $this->config['username'],
112
-            'password' => $this->config['password'],
113
-        );
114
-    }
115
-
116
-    /**
117
-     * Configures environment
118
-     *
119
-     * @param array  $config Configuration
120
-     * @param string $title  Source identifier
121
-     */
122
-    public function configure($config, $title = null)
123
-    {
124
-        if (!empty($config['host'])) {
125
-            $config['baseuri'] = $config['host'];
126
-        }
127
-
128
-        $this->config = array_merge($this->config, $config);
129
-        $this->title  = $title;
130
-    }
131
-
132
-    /**
133
-     * Initializes WebDAV client
134
-     */
135
-    protected function init()
136
-    {
137
-        if ($this->client !== null) {
138
-            return true;
139
-        }
140
-
141
-        // Load configuration for main driver
142
-        $config['baseuri'] = $this->rc->config->get('fileapi_webdav_baseuri');
143
-
144
-        if (!empty($config['baseuri'])) {
145
-            $config['username'] = $_SESSION['username'];
146
-            $config['password'] = $this->rc->decrypt($_SESSION['password']);
147
-        }
148
-
149
-        $this->config = array_merge($config, $this->config);
150
-
151
-        // Use session username if not set in configuration
152
-        if (!isset($this->config['username'])) {
153
-            $this->config['username'] = $_SESSION[$this->title . '_webdav_user'];
154
-        }
155
-        if (!isset($this->config['password'])) {
156
-            $this->config['password'] = $this->rc->decrypt($_SESSION[$this->title . '_webdav_pass']);
157
-        }
158
-
159
-        if (empty($this->config['baseuri'])) {
160
-            throw new Exception("Missing base URI of WebDAV server", file_storage::ERROR_NOAUTH);
161
-        }
162
-
163
-        $this->client = new Client(array(
164
-            'baseUri'  => rtrim($this->config['baseuri'], '/') . '/',
165
-            'userName' => $this->config['username'],
166
-            'password' => $this->config['password'],
167
-            'authType' => Client::AUTH_BASIC,
168
-        ));
169
-    }
170
-
171
-    /**
172
-     * Returns current instance title
173
-     *
174
-     * @return string Instance title (mount point)
175
-     */
176
-    public function title()
177
-    {
178
-        return $this->title;
179
-    }
180
-
181
-    /**
182
-     * Storage driver capabilities
183
-     *
184
-     * @return array List of capabilities
185
-     */
186
-    public function capabilities()
187
-    {
188
-        // find max filesize value
189
-        $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
190
-        $max_postsize = parse_bytes(ini_get('post_max_size'));
191
-        if ($max_postsize && $max_postsize < $max_filesize) {
192
-            $max_filesize = $max_postsize;
193
-        }
194
-
195
-        return array(
196
-            file_storage::CAPS_MAX_UPLOAD => $max_filesize,
197
-            file_storage::CAPS_QUOTA      => true,
198
-            file_storage::CAPS_LOCKS      => true, //TODO: Implement WebDAV locks
199
-        );
200
-    }
201
-
202
-    /**
203
-     * Save configuration of external driver (mount point)
204
-     *
205
-     * @param array $driver Driver data
206
-     *
207
-     * @throws Exception
208
-     */
209
-    public function driver_create($driver)
210
-    {
211
-        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
212
-    }
213
-
214
-    /**
215
-     * Delete configuration of external driver (mount point)
216
-     *
217
-     * @param string $name Driver instance name
218
-     *
219
-     * @throws Exception
220
-     */
221
-    public function driver_delete($name)
222
-    {
223
-        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
224
-    }
225
-
226
-    /**
227
-     * Return list of registered drivers (mount points)
228
-     *
229
-     * @return array List of drivers data
230
-     * @throws Exception
231
-     */
232
-    public function driver_list()
233
-    {
234
-        return array();
235
-        //TODO: Stub. Not implemented.
236
-    }
237
-
238
-    /**
239
-     * Update configuration of external driver (mount point)
240
-     *
241
-     * @param string $title  Driver instance title
242
-     * @param array  $driver Driver data
243
-     *
244
-     * @throws Exception
245
-     */
246
-    public function driver_update($title, $driver)
247
-    {
248
-        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
249
-    }
250
-
251
-    /**
252
-     * Returns metadata of the driver
253
-     *
254
-     * @return array Driver meta data (image, name, form)
255
-     */
256
-    public function driver_metadata()
257
-    {
258
-        $image_content = file_get_contents(__DIR__ . '/webdav.png');
259
-
260
-        $metadata = array(
261
-            'image' => 'data:image/png;base64,' . base64_encode($image_content),
262
-            'name'  => 'WebDAV',
263
-            'ref'   => 'http://www.webdav.org/',
264
-            'description' => 'WebDAV client',
265
-            'form'  => array(
266
-                'baseuri'  => 'baseuri',
267
-                'username' => 'username',
268
-                'password' => 'password',
269
-            ),
270
-        );
271
-
272
-        // these are returned when authentication on folders list fails
273
-        if ($this->config['username']) {
274
-            $metadata['form_values'] = array(
275
-                'baseuri'  => $this->config['baseuri'],
276
-                'username' => $this->config['username'],
277
-            );
278
-        }
279
-
280
-        return $metadata;
281
-    }
282
-
283
-    /**
284
-     * Validate metadata (config) of the driver
285
-     *
286
-     * @param array $metadata Driver metadata
287
-     *
288
-     * @return array Driver meta data to be stored in configuration
289
-     * @throws Exception
290
-     */
291
-    public function driver_validate($metadata)
292
-    {
293
-        if (!is_string($metadata['username']) || !strlen($metadata['username'])) {
294
-            throw new Exception("Missing user name.", file_storage::ERROR);
295
-        }
296
-
297
-        if (!is_string($metadata['password']) || !strlen($metadata['password'])) {
298
-            throw new Exception("Missing user password.", file_storage::ERROR);
299
-        }
300
-
301
-        if (!is_string($metadata['baseuri']) || !strlen($metadata['baseuri'])) {
302
-            throw new Exception("Missing base URL.", file_storage::ERROR);
303
-        }
304
-
305
-        // Ensure baseUri ends with a slash
306
-        $base_uri = $metadata['baseuri'];
307
-        if (substr($base_uri, -1) != '/') {
308
-            $base_uri .= '/';
309
-        }
310
-
311
-        $this->config['baseuri'] = $base_uri;
312
-
313
-        if (!$this->authenticate($metadata['username'], $metadata['password'])) {
314
-            throw new Exception("Unable to authenticate user", file_storage::ERROR_NOAUTH);
315
-        }
316
-
317
-        return array(
318
-            'host'     => $base_uri,
319
-            'port'     => 0,
320
-            'username' => $metadata['username'],
321
-            'password' => $metadata['password'],
322
-        );
323
-    }
324
-
325
-    /**
326
-     * Create a file.
327
-     *
328
-     * @param string $file_name Name of a file (with folder path)
329
-     * @param array  $file      File data (path, type)
330
-     *
331
-     * @throws Exception
332
-     */
333
-    public function file_create($file_name, $file)
334
-    {
335
-        $this->init();
336
-
337
-        if ($file['path']) {
338
-            $data = fopen($file['path'], 'r');
339
-        }
340
-        else {
341
-            // Resource or data
342
-            $data = $file['content'];
343
-        }
344
-
345
-        if (is_resource($data)) {
346
-            // Need to tell Curl the attachments size, so it properly
347
-            // sets Content-Length header, that is required in PUT
348
-            // request by some webdav servers (#2978)
349
-            $stat = fstat($data);
350
-            $this->client->addCurlSetting(CURLOPT_INFILESIZE, $stat['size']);
351
-        }
352
-
353
-        $file_name = $this->encode_path($file_name);
354
-        $response  = $this->client->request('PUT', $file_name, $data);
355
-
356
-        if ($file['path']) {
357
-            fclose($data);
358
-        }
359
-
360
-        if ($response['statusCode'] != 201) {
361
-            throw new Exception("Storage error. " . $response['body'], file_storage::ERROR);
362
-        }
363
-    }
364
-
365
-    /**
366
-     * Update a file.
367
-     *
368
-     * @param string $file_name Name of a file (with folder path)
369
-     * @param array  $file      File data (path, type)
370
-     *
371
-     * @throws Exception
372
-     */
373
-    public function file_update($file_name, $file)
374
-    {
375
-        $this->init();
376
-
377
-        if ($file['path']) {
378
-            $data = fopen($file['path'], 'r');
379
-        }
380
-        else {
381
-            //Resource or data
382
-            $data = $file['content'];
383
-        }
384
-
385
-        if (is_resource($data)) {
386
-            // Need to tell Curl the attachment size, so it properly
387
-            // sets Content-Length header, that is required in PUT
388
-            // request by some webdav servers (#2978)
389
-            $stat = fstat($data);
390
-            $this->client->addCurlSetting(CURLOPT_INFILESIZE, $stat['size']);
391
-        }
392
-
393
-        $file_name = $this->encode_path($file_name);
394
-        $response  = $this->client->request('PUT', $file_name, $data);
395
-
396
-        if ($file['path']) {
397
-            fclose($data);
398
-        }
399
-
400
-        if ($response['statusCode'] != 204) {
401
-            throw new Exception("Storage error. " . $response['body'], file_storage::ERROR);
402
-        }
403
-    }
404
-
405
-    /**
406
-     * Delete a file.
407
-     *
408
-     * @param string $file_name Name of a file (with folder path)
409
-     *
410
-     * @throws Exception
411
-     */
412
-    public function file_delete($file_name)
413
-    {
414
-        $this->init();
415
-
416
-        $file_name = $this->encode_path($file_name);
417
-        $response  = $this->client->request('DELETE', $file_name);
418
-
419
-        if ($response['statusCode'] != 204) {
420
-            throw new Exception("Storage error: " . $response['body'], file_storage::ERROR);
421
-        }
422
-    }
423
-
424
-    /**
425
-     * Return file body.
426
-     *
427
-     * @param string   $file_name Name of a file (with folder path)
428
-     * @param array    $params    Parameters (force-download)
429
-     * @param resource $fp        Print to file pointer instead (send no headers)
430
-     *
431
-     * @throws Exception
432
-     */
433
-    public function file_get($file_name, $params = array(), $fp = null)
434
-    {
435
-        $this->init();
436
-
437
-        // @TODO: Write directly to $fp
438
-
439
-        $file_name = $this->encode_path($file_name);
440
-        $response  = $this->client->request($params['head'] ? 'HEAD' : 'GET', $file_name);
441
-
442
-        if ($response['statusCode'] != 200) {
443
-            throw new Exception("Storage error. File not found.", file_storage::ERROR);
444
-        }
445
-
446
-        // Sometimes Content-Length is an array here (T3757)
447
-        $size = $response['headers']['content-length'];
448
-        if (is_array($size)) {
449
-            $size = $size[0];
450
-        }
451
-
452
-        // write to file pointer, send no headers
453
-        if ($fp) {
454
-            if ($size) {
455
-                fwrite($fp, $response['body']);
456
-            }
457
-            return;
458
-        }
459
-
460
-        if (!empty($params['force-download'])) {
461
-            $disposition = 'attachment';
462
-            header("Content-Type: application/octet-stream");
463
-// @TODO
464
-//            if ($browser->ie)
465
-//                header("Content-Type: application/force-download");
466
-        }
467
-        else {
468
-            $mimetype = file_utils::real_mimetype($params['force-type'] ? $params['force-type'] : $response['headers']['content-type']);
469
-            $disposition = 'inline';
470
-
471
-            header("Content-Transfer-Encoding: binary");
472
-            header("Content-Type: $mimetype");
473
-        }
474
-
475
-        $filename = addcslashes(end(explode('/', $file_name)), '"');
476
-
477
-        // Workaround for nasty IE bug (#1488844)
478
-        // If Content-Disposition header contains string "attachment" e.g. in filename
479
-        // IE handles data as attachment not inline
480
-/*
481
-@TODO
482
-        if ($disposition == 'inline' && $browser->ie && $browser->ver < 9) {
483
-            $filename = str_ireplace('attachment', 'attach', $filename);
484
-        }
485
-*/
486
-        header("Content-Length: " . $size);
487
-        header("Content-Disposition: $disposition; filename=\"$filename\"");
488
-
489
-        if ($size && empty($params['head'])) {
490
-            echo $response['body'];
491
-        }
492
-    }
493
-
494
-    /**
495
-     * Returns file metadata.
496
-     *
497
-     * @param string $file_name Name of a file (with folder path)
498
-     *
499
-     * @throws Exception
500
-     */
501
-    public function file_info($file_name)
502
-    {
503
-        $this->init();
504
-
505
-        try {
506
-            $props = $this->client->propfind($this->encode_path($file_name), array(
507
-                    '{DAV:}resourcetype',
508
-                    '{DAV:}getcontentlength',
509
-                    '{DAV:}getcontenttype',
510
-                    '{DAV:}getlastmodified',
511
-                    '{DAV:}creationdate'
512
-                ), 0);
513
-        }
514
-        catch (Exception $e) {
515
-            throw new Exception("Storage error. File not found.", file_storage::ERROR);
516
-        }
517
-
518
-        $mtime = new DateTime($props['{DAV:}getlastmodified']);
519
-        $ctime = new DateTime($props['{DAV:}creationdate']);
520
-
521
-        return array (
522
-            'name'     => end(explode('/', $file_name)),
523
-            'size'     => (int) $props['{DAV:}getcontentlength'],
524
-            'type'     => (string) $props['{DAV:}getcontenttype'],
525
-            'mtime'    => file_utils::date_format($mtime, $this->config['date_format'], $this->config['timezone']),
526
-            'ctime'    => file_utils::date_format($ctime, $this->config['date_format'], $this->config['timezone']),
527
-            'modified' => $mtime ? $mtime->format('U') : 0,
528
-            'created'  => $ctime ? $ctime->format('U') : 0,
529
-        );
530
-    }
531
-
532
-    /**
533
-     * List files in a folder.
534
-     *
535
-     * @param string $folder_name Name of a folder with full path
536
-     * @param array  $params      List parameters ('sort', 'reverse', 'search', 'prefix')
537
-     *
538
-     * @return array List of files (file properties array indexed by filename)
539
-     * @throws Exception
540
-     */
541
-    public function file_list($folder_name, $params = array())
542
-    {
543
-        $this->init();
544
-
545
-        if (!empty($params['search'])) {
546
-            foreach ($params['search'] as $idx => $value) {
547
-                switch ($idx) {
548
-                case 'name':
549
-                    $params['search']['name'] = mb_strtoupper($value);
550
-                    break;
551
-
552
-                case 'class':
553
-                    $params['search']['class'] = file_utils::class2mimetypes($params['search']['class']);
554
-                    break;
555
-                }
556
-            }
557
-        }
558
-
559
-        try {
560
-            $items = $this->client->propfind($this->encode_path($folder_name), array(
561
-                    '{DAV:}resourcetype',
562
-                    '{DAV:}getcontentlength',
563
-                    '{DAV:}getcontenttype',
564
-                    '{DAV:}getlastmodified',
565
-                    '{DAV:}creationdate'
566
-                ), 1);
567
-        }
568
-        catch (Exception $e) {
569
-            throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
570
-        }
571
-
572
-        $result = array();
573
-
574
-        foreach ($items as $file => $props) {
575
-            // Skip directories
576
-            $is_dir = !empty($props['{DAV:}resourcetype']) && count($props['{DAV:}resourcetype']->getValue()) > 0;
577
-            if ($is_dir) {
578
-                continue;
579
-            }
580
-
581
-            $mtime = new DateTime($props['{DAV:}getlastmodified']);
582
-            $ctime = new DateTime($props['{DAV:}creationdate']);
583
-            $ctype = (string) $props['{DAV:}getcontenttype'];
584
-
585
-            $path  = $this->get_full_url($file);
586
-            $path  = $this->decode_path($path);
587
-            $fname = end(explode('/', $path));
588
-
589
-            if (!empty($params['search'])) {
590
-                foreach ($params['search'] as $idx => $value) {
591
-                    switch ($idx) {
592
-                    case 'name':
593
-                        if (stripos(mb_strtoupper($fname), $value) === false) {
594
-                            continue 3; // skip the file
595
-                        }
596
-                        break;
597
-
598
-                    case 'class':
599
-                        foreach ($value as $type) {
600
-                            if (stripos($ctype, $type) !== false) {
601
-                                continue 3;
602
-                            }
603
-                        }
604
-                        continue 3; // skip the file
605
-                        break;
606
-                    }
607
-                }
608
-            }
609
-
610
-            $result[$path] = array(
611
-                'name'     => $fname,
612
-                'size'     => (int) $props['{DAV:}getcontentlength'],
613
-                'type'     => $ctype,
614
-                'mtime'    => file_utils::date_format($mtime, $this->config['date_format'], $this->config['timezone']),
615
-                'ctime'    => file_utils::date_format($ctime, $this->config['date_format'], $this->config['timezone']),
616
-                'modified' => $mtime ? $mtime->format('U') : 0,
617
-                'created'  => $ctime ? $ctime->format('U') : 0,
618
-            );
619
-        }
620
-
621
-        // @TODO: pagination
622
-
623
-        // Sorting
624
-        $sort  = !empty($params['sort']) ? $params['sort'] : 'name';
625
-        $index = array();
626
-
627
-        if ($sort == 'mtime') {
628
-            $sort = 'modified';
629
-        }
630
-
631
-        if (in_array($sort, array('name', 'size', 'modified'))) {
632
-            foreach ($result as $key => $val) {
633
-                $index[$key] = $val[$sort];
634
-            }
635
-            array_multisort($index, SORT_ASC, SORT_NUMERIC, $result);
636
-        }
637
-
638
-        if ($params['reverse']) {
639
-            $result = array_reverse($result, true);
640
-        }
641
-
642
-        return $result;
643
-    }
644
-
645
-    /**
646
-     * Copy a file.
647
-     *
648
-     * @param string $file_name Name of a file (with folder path)
649
-     * @param string $new_name  New name of a file (with folder path)
650
-     *
651
-     * @throws Exception
652
-     */
653
-    public function file_copy($file_name, $new_name)
654
-    {
655
-        $this->init();
656
-
657
-        $request   =  array('Destination' => $this->config['baseuri'] . '/' . rawurlencode($new_name));
658
-        $file_name = $this->encode_path($file_name);
659
-        $response  = $this->client->request('COPY', $file_name, null, $request);
660
-
661
-        if ($response['statusCode'] != 201) {
662
-            throw new Exception("Storage error: " . $response['body'], file_storage::ERROR);
663
-        }
664
-    }
665
-
666
-    /**
667
-     * Move (or rename) a file.
668
-     *
669
-     * @param string $file_name Name of a file (with folder path)
670
-     * @param string $new_name  New name of a file (with folder path)
671
-     *
672
-     * @throws Exception
673
-     */
674
-    public function file_move($file_name, $new_name)
675
-    {
676
-        $this->init();
677
-
678
-        $request   = array('Destination' => $this->config['baseuri'] . '/' . rawurlencode($new_name));
679
-        $file_name = $this->encode_path($file_name);
680
-        $response  = $this->client->request('MOVE', $file_name, null, $request);
681
-
682
-        if ($response['statusCode'] != 201) {
683
-            throw new Exception("Storage error: " . $response['body'], file_storage::ERROR);
684
-        }
685
-    }
686
-
687
-    /**
688
-     * Create a folder.
689
-     *
690
-     * @param string $folder_name Name of a folder with full path
691
-     *
692
-     * @throws Exception on error
693
-     */
694
-    public function folder_create($folder_name)
695
-    {
696
-        $this->init();
697
-
698
-        $folder_name = $this->encode_path($folder_name);
699
-        $response    = $this->client->request('MKCOL', $folder_name);
700
-
701
-        if ($response['statusCode'] != 201) {
702
-            throw new Exception("Storage error: " . $response['body'], file_storage::ERROR);
703
-        }
704
-    }
705
-
706
-    /**
707
-     * Delete a folder.
708
-     *
709
-     * @param string $folder_name Name of a folder with full path
710
-     *
711
-     * @throws Exception on error
712
-     */
713
-    public function folder_delete($folder_name)
714
-    {
715
-        $this->init();
716
-
717
-        $folder_name = $this->encode_path($folder_name);
718
-        $response    = $this->client->request('DELETE', $folder_name);
719
-
720
-        if ($response['statusCode'] != 204) {
721
-            throw new Exception("Storage error: " . $response['body'], file_storage::ERROR);
722
-        }
723
-    }
724
-
725
-    /**
726
-     * Move/Rename a folder.
727
-     *
728
-     * @param string $folder_name Name of a folder with full path
729
-     * @param string $new_name    New name of a folder with full path
730
-     *
731
-     * @throws Exception on error
732
-     */
733
-    public function folder_move($folder_name, $new_name)
734
-    {
735
-        $this->init();
736
-
737
-        $request     = array('Destination' => $this->config['baseuri'] . '/' . rawurlencode($new_name));
738
-        $folder_name = $this->encode_path($folder_name);
739
-        $response    = $this->client->request('MOVE', $folder_name, null, $request);
740
-
741
-        if ($response['statusCode'] != 201) {
742
-            throw new Exception("Storage error: " . $response['body'], file_storage::ERROR);
743
-        }
744
-    }
745
-
746
-    /**
747
-     * Subscribe a folder.
748
-     *
749
-     * @param string $folder_name Name of a folder with full path
750
-     *
751
-     * @throws Exception
752
-     */
753
-    public function folder_subscribe($folder_name)
754
-    {
755
-        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
756
-    }
757
-
758
-    /**
759
-     * Unsubscribe a folder.
760
-     *
761
-     * @param string $folder_name Name of a folder with full path
762
-     *
763
-     * @throws Exception
764
-     */
765
-    public function folder_unsubscribe($folder_name)
766
-    {
767
-        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
768
-    }
769
-
770
-    /**
771
-     * Returns list of folders.
772
-     *
773
-     * @param array $params List parameters ('type', 'search', path, level)
774
-     *
775
-     * @return array List of folders
776
-     * @throws Exception
777
-     */
778
-    public function folder_list($params = array())
779
-    {
780
-        $this->init();
781
-
782
-        if (empty($params['level'])) {
783
-            $params['level'] = 100;
784
-        }
785
-
786
-        $result = $this->folders_tree($params);
787
-
788
-        // ensure sorted folders
789
-        usort($result, array('file_utils', 'sort_folder_comparator'));
790
-
791
-        // In extended format we return array of arrays
792
-        if (!empty($params['extended'])) {
793
-            foreach ($result as $idx => $folder) {
794
-                $item = array('folder' => $folder);
795
-                $result[$idx] = $item;
796
-            }
797
-        }
798
-
799
-        return $result;
800
-    }
801
-
802
-    /**
803
-     * Check folder rights.
804
-     *
805
-     * @param string $folder_name Name of a folder with full path
806
-     *
807
-     * @return int Folder rights (sum of file_storage::ACL_*)
808
-     */
809
-    public function folder_rights($folder_name)
810
-    {
811
-        // @TODO
812
-        return file_storage::ACL_READ | file_storage::ACL_WRITE;
813
-    }
814
-
815
-    /**
816
-     * Returns a list of locks
817
-     *
818
-     * This method should return all the locks for a particular URI, including
819
-     * locks that might be set on a parent URI.
820
-     *
821
-     * If child_locks is set to true, this method should also look for
822
-     * any locks in the subtree of the URI for locks.
823
-     *
824
-     * @param string $path        File/folder path
825
-     * @param bool   $child_locks Enables subtree checks
826
-     *
827
-     * @return array List of locks
828
-     * @throws Exception
829
-     */
830
-    public function lock_list($path, $child_locks = false)
831
-    {
832
-        $this->init_lock_db();
833
-
834
-        // convert path into global URI
835
-        $uri = $this->path2uri($path);
836
-
837
-        // get locks list
838
-        $list = $this->lock_db->lock_list($uri, $child_locks);
839
-
840
-        // convert back global URIs into paths
841
-        foreach ($list as $idx => $lock) {
842
-            $list[$idx]['uri'] = $this->uri2path($lock['uri']);
843
-        }
844
-
845
-        return $list;
846
-    }
847
-
848
-    /**
849
-     * Locks a URI
850
-     *
851
-     * @param string $path File/folder path
852
-     * @param array  $lock Lock data
853
-     *                     - depth: 0/'infinite'
854
-     *                     - scope: 'shared'/'exclusive'
855
-     *                     - owner: string
856
-     *                     - token: string
857
-     *                     - timeout: int
858
-     *
859
-     * @throws Exception
860
-     */
861
-    public function lock($path, $lock)
862
-    {
863
-        $this->init_lock_db();
864
-
865
-        // convert path into global URI
866
-        $uri = $this->path2uri($path);
867
-
868
-        if (!$this->lock_db->lock($uri, $lock)) {
869
-            throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR);
870
-        }
871
-    }
872
-
873
-    /**
874
-     * Removes a lock from a URI
875
-     *
876
-     * @param string $path File/folder path
877
-     * @param array  $lock Lock data
878
-     *
879
-     * @throws Exception
880
-     */
881
-    public function unlock($path, $lock)
882
-    {
883
-        $this->init_lock_db();
884
-
885
-        // convert path into global URI
886
-        $uri = $this->path2uri($path);
887
-
888
-        if (!$this->lock_db->unlock($uri, $lock)) {
889
-            throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR);
890
-        }
891
-    }
892
-
893
-    /**
894
-     * Return disk quota information for specified folder.
895
-     *
896
-     * @param string $folder_name Name of a folder with full path
897
-     *
898
-     * @return array Quota
899
-     * @throws Exception
900
-     */
901
-    public function quota($folder)
902
-    {
903
-        $this->init();
904
-
905
-        $props = $this->client->propfind($this->encode_path($folder), array(
906
-            '{DAV:}quota-available-bytes',
907
-            '{DAV:}quota-used-bytes',
908
-        ), 0);
909
-
910
-        $used      = $props['{DAV:}quota-used-bytes'];
911
-        $available = $props['{DAV:}quota-available-bytes'];
912
-
913
-        return array(
914
-            // expected values in kB
915
-            'total' => ($used + $available) / 1024,
916
-            'used'  => $used / 1024,
917
-        );
918
-    }
919
-
920
-    /**
921
-     * Sharing interface
922
-     *
923
-     * @param string $folder_name Name of a folder with full path
924
-     * @param int    $mode        Sharing action mode
925
-     * @param array  $args        POST/GET parameters
926
-     *
927
-     * @return mixed Sharing response
928
-     * @throws Exception
929
-     */
930
-    public function sharing($folder, $mode, $args = array())
931
-    {
932
-        // TODO
933
-        throw new Exception("Search not implemented", file_storage::ERROR_UNSUPPORTED);
934
-    }
935
-
936
-    /**
937
-     * User/group search (autocompletion)
938
-     *
939
-     * @param string $search Search string
940
-     * @param int    $mode   Search mode
941
-     *
942
-     * @return array Users/Groups list
943
-     * @throws Exception
944
-     */
945
-    public function autocomplete($search, $mode)
946
-    {
947
-        // TODO
948
-        throw new Exception("Search not implemented", file_storage::ERROR_UNSUPPORTED);
949
-    }
950
-
951
-    /**
952
-     * Convert file/folder path into a global URI.
953
-     *
954
-     * @param string $path File/folder path
955
-     *
956
-     * @return string URI
957
-     * @throws Exception
958
-     */
959
-    public function path2uri($path)
960
-    {
961
-        $base = preg_replace('|^[a-zA-Z]+://|', '', $this->config['baseuri']);
962
-
963
-        return 'webdav://' . rawurlencode($this->config['username']) . '@' . $base
964
-            . '/' . file_utils::encode_path($path);
965
-    }
966
-
967
-    /**
968
-     * Convert global URI into file/folder path.
969
-     *
970
-     * @param string $uri URI
971
-     *
972
-     * @return string File/folder path
973
-     * @throws Exception
974
-     */
975
-    public function uri2path($uri)
976
-    {
977
-        if (!preg_match('|^webdav://([^@]+)@(.*)$|', $uri, $matches)) {
978
-            throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR);
979
-        }
980
-
981
-        $user = rawurldecode($matches[1]);
982
-        $base = preg_replace('|^[a-zA-Z]+://|', '', $this->config['baseuri']);
983
-        $uri  = $matches[2];
984
-
985
-        if ($user != $this->config['username'] || strpos($uri, $base) !== 0) {
986
-            throw new Exception("Internal storage error. Unresolvable URI.", file_storage::ERROR);
987
-        }
988
-
989
-        $uri = substr($matches[2], strlen($base) + 1);
990
-
991
-        return file_utils::decode_path($uri);
992
-    }
993
-
994
-    /**
995
-     * Gets the relative URL of a resource
996
-     *
997
-     * @param string $url WebDAV URL
998
-     * @return string Path relative to root (title/.)
999
-     */
1000
-    protected function get_relative_url($url)
1001
-    {
1002
-        $url = $this->client->getAbsoluteUrl($url);
1003
-
1004
-        return trim(str_replace($this->config['baseuri'], '', $url), '/');
1005
-    }
1006
-
1007
-    /**
1008
-     * Gets the full URL of a resource
1009
-     *
1010
-     * @param string $url WebDAV URL
1011
-     * @return string Path relative to chwala root
1012
-     */
1013
-    protected function get_full_url($url)
1014
-    {
1015
-        if (!empty($this->title)) {
1016
-            return $this->title . '/' . $this->get_relative_url($url);
1017
-        }
1018
-
1019
-        return $this->get_relative_url($url);
1020
-    }
1021
-
1022
-    /**
1023
-     * Encode folder/file names in the path
1024
-     * so it can be used as URL
1025
-     *
1026
-     * @param string $path File/folder path
1027
-     *
1028
-     * @return string Encoded URL
1029
-     */
1030
-    protected function encode_path($path)
1031
-    {
1032
-        $path = explode('/', $path);
1033
-        $path = array_map('rawurlencode', $path);
1034
-
1035
-        return implode('/', $path);
1036
-    }
1037
-
1038
-    /**
1039
-     * Decode folder/file URL path
1040
-     *
1041
-     * @param string $path File/folder path
1042
-     *
1043
-     * @return string Decoded path
1044
-     */
1045
-    protected function decode_path($path)
1046
-    {
1047
-        $path = explode('/', $path);
1048
-        $path = array_map('rawurldecode', $path);
1049
-
1050
-        return implode('/', $path);
1051
-    }
1052
-
1053
-    /**
1054
-     * Recursive method to fetch folders tree
1055
-     */
1056
-    protected function folders_tree($params)
1057
-    {
1058
-        $folders = array();
1059
-        $root    = '';
1060
-
1061
-        if (is_string($params['path']) && strlen($params['path'])) {
1062
-            $root = trim($params['path'], '/');
1063
-        }
1064
-
1065
-        try {
1066
-            $props = array('{DAV:}resourcetype');
1067
-            $items = $this->client->propfind($root, $props, '1,noroot');
1068
-        }
1069
-        catch (Exception $e) {
1070
-            throw new Exception("User credentials not provided", file_storage::ERROR_NOAUTH);
1071
-        }
1072
-
1073
-        foreach ($items as $file => $props) {
1074
-            // Skip files
1075
-            $is_dir = !empty($props['{DAV:}resourcetype']) && count($props['{DAV:}resourcetype']->getValue()) > 0;
1076
-            if (!$is_dir) {
1077
-                continue;
1078
-            }
1079
-
1080
-            $path = $this->get_relative_url($file);
1081
-            $path = $this->decode_path($path);
1082
-
1083
-            // 'noroot' not always works
1084
-            if ($path === $root) {
1085
-                continue;
1086
-            }
1087
-
1088
-            $folders[] = $path;
1089
-
1090
-            $params['level'] -= 1;
1091
-
1092
-            // Many servers just do not support 'Depth: infinity' for security reasons
1093
-            // E.g. SabreDAV has this optional and disabled by default
1094
-
1095
-            if ($params['level'] > 0) {
1096
-                $params['path'] = $path;
1097
-                $tree = $this->folders_tree($params);
1098
-                if (!empty($tree)) {
1099
-                    $folders = array_merge($folders, $tree);
1100
-                }
1101
-            }
1102
-        }
1103
-
1104
-        return $folders;
1105
-    }
1106
-
1107
-    /**
1108
-     * Initializes file_locks object
1109
-     */
1110
-    protected function init_lock_db()
1111
-    {
1112
-        if (!$this->lock_db) {
1113
-            $this->lock_db = new file_locks;
1114
-        }
1115
-    }
1116
-}
1117
iRony-0.4.3.tar.gz/lib/FileAPI/file_api_wopi.php Deleted
124
 
1
@@ -1,122 +0,0 @@
2
-<?php
3
-/*
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2018, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-class file_api_wopi extends file_api
27
-{
28
-    public $output_type = file_api_core::OUTPUT_JSON;
29
-    public $path        = array();
30
-    public $args        = array();
31
-    public $method      = 'GET';
32
-
33
-
34
-    /**
35
-     * Session validation check and session start
36
-     */
37
-    protected function session_validate($new_session = false, $token = null)
38
-    {
39
-        if (empty($_GET['access_token'])) {
40
-            throw new Exception("Token missing", file_api_core::ERROR_UNAUTHORIZED);
41
-        }
42
-
43
-        return parent::session_validate($new_session = false, $_GET['access_token']);
44
-    }
45
-
46
-    /**
47
-     * Storage/System method handler
48
-     */
49
-    protected function request_handler($request)
50
-    {
51
-        // header("X-WOPI-HostEndpoint: " . $endpoint_desc);
52
-        // header("X-WOPI-MachineName: " .  $machine_name);
53
-        header("X-WOPI-ServerVersion: " . file_api_core::API_VERSION);
54
-
55
-        $request = $_GET['method']; // file_api uses strtolower(), we don't want that
56
-
57
-        // handle request
58
-        if ($request && preg_match('/^[a-z]+\/*[a-zA-Z0-9_\/-]*$/', $request)) {
59
-            $path    = explode('/', $request);
60
-            $request = array_shift($path);
61
-            $method  = $_SERVER['REQUEST_METHOD'];
62
-
63
-            if ($_method = rcube_utils::request_header('X-WOPI-Override')) {
64
-                $method = $_method;
65
-            }
66
-            else if ($method == 'POST' && !empty($_SERVER['HTTP_X_HTTP_METHOD'])) {
67
-                $method = $_SERVER['HTTP_X_HTTP_METHOD'];
68
-            }
69
-
70
-            $this->path   = $path;
71
-            $this->args   = $_GET;
72
-            $this->method = $method;
73
-
74
-            include_once __DIR__ . "/wopi/$request.php";
75
-
76
-            $class_name = "file_wopi_$request";
77
-            if (class_exists($class_name, false)) {
78
-                $handler = new $class_name($this);
79
-                return $handler->handle();
80
-            }
81
-        }
82
-
83
-        throw new Exception("Unknown method", file_api_core::ERROR_NOT_FOUND);
84
-    }
85
-
86
-    /**
87
-     * Send success response
88
-     *
89
-     * @param mixed $data Data
90
-     */
91
-    public function output_success($data)
92
-    {
93
-        $this->output_send($data);
94
-    }
95
-
96
-    /**
97
-     * Send error response
98
-     *
99
-     * @param mixed $response Response data
100
-     * @param int   $code     Error code
101
-     */
102
-    public function output_error($response, $code = null)
103
-    {
104
-        header(sprintf("HTTP/1.0 %d %s", $code ?: file_api_core::ERROR_CODE, $response));
105
-
106
-        $this->output_send();
107
-    }
108
-
109
-    /**
110
-     * Send response
111
-     *
112
-     * @param mixed $data Data
113
-     */
114
-    public function output_send($data = null)
115
-    {
116
-        // Remove NULL data according to WOPI spec.
117
-        if (is_array($data)) {
118
-            $data = array_filter($data, function($v) { return $v !== null; });
119
-        }
120
-
121
-        parent::output_send($data);
122
-    }
123
-}
124
iRony-0.4.3.tar.gz/lib/FileAPI/file_document.php Deleted
882
 
1
@@ -1,880 +0,0 @@
2
-<?php
3
-/**
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2016, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-/**
27
- * Document editing sessions handling
28
- */
29
-class file_document
30
-{
31
-    protected $api;
32
-    protected $rc;
33
-    protected $user;
34
-    protected $sessions_table    = 'chwala_sessions';
35
-    protected $invitations_table = 'chwala_invitations';
36
-    protected $icache            = array();
37
-    protected $file_meta_items   = array('type', 'name', 'size', 'modified');
38
-
39
-    const STATUS_INVITED        = 'invited';
40
-    const STATUS_REQUESTED      = 'requested';
41
-    const STATUS_ACCEPTED       = 'accepted';
42
-    const STATUS_DECLINED       = 'declined';
43
-    const STATUS_DECLINED_OWNER = 'declined-owner'; // same as 'declined' but done by the session owner
44
-    const STATUS_ACCEPTED_OWNER = 'accepted-owner'; // same as 'accepted' but done by the session owner
45
-
46
-    const DB_DATE_FORMAT = 'Y-m-d H:i:s';
47
-
48
-
49
-    /**
50
-     * Class constructor
51
-     *
52
-     * @param file_api $api Chwala API app instance
53
-     */
54
-    public function __construct($api)
55
-    {
56
-        $this->rc   = rcube::get_instance();
57
-        $this->api  = $api;
58
-        $this->user = $_SESSION['user'];
59
-
60
-        $db = $this->rc->get_dbh();
61
-        $this->sessions_table    = $db->table_name($this->sessions_table);
62
-        $this->invitations_table = $db->table_name($this->invitations_table);
63
-    }
64
-
65
-    /**
66
-     * Detect type of file_document class to use for specified session
67
-     *
68
-     * @param file_api $api        Chwala API app instance
69
-     * @param string   $session_id Document session ID
70
-     *
71
-     * @return file_document Document object
72
-     */
73
-    public static function get_handler($api, $session_id)
74
-    {
75
-        // we add "w-" prefix to wopi session identifiers,
76
-        // so we can distinguish it from manticore sessions
77
-        if (strpos($session_id, 'w-') === 0) {
78
-            return new file_wopi($api);
79
-        }
80
-
81
-        return new file_manticore($api);
82
-    }
83
-
84
-    /**
85
-     * Return viewer URI for specified file/session. This creates
86
-     * a new collaborative editing session when needed.
87
-     *
88
-     * @param string $file        File path
89
-     * @param array  &$file_info  File metadata (e.g. type)
90
-     * @param string &$session_id Optional session ID to join to
91
-     * @param string $readonly    Create readonly (one-time) session
92
-     *
93
-     * @return string An URI for specified file/session
94
-     * @throws Exception
95
-     */
96
-    public function session_start($file, &$file_info, &$session_id = null, $readonly = false)
97
-    {
98
-        if ($file !== null) {
99
-            $uri = $this->path2uri($file, $driver);
100
-        }
101
-
102
-        $backend = $this->api->get_backend();
103
-
104
-        if ($session_id) {
105
-            $session = $this->session_info($session_id);
106
-
107
-            if (empty($session)) {
108
-                throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
109
-            }
110
-
111
-            // check session ownership
112
-            if ($session['owner'] != $this->user) {
113
-                // check if the user was invited
114
-                $invitations = $this->invitations_find(array('session_id' => $session_id, 'user' => $this->user));
115
-                $states      = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
116
-
117
-                if (empty($invitations) || !in_array($invitations[0]['status'], $states)) {
118
-                    throw new Exception("No permission to join the editing session.", file_api_core::ERROR_CODE);
119
-                }
120
-
121
-                // automatically accept the invitation, if not done yet
122
-                if ($invitations[0]['status'] == self::STATUS_INVITED) {
123
-                    $this->invitation_update($session_id, $this->user, self::STATUS_ACCEPTED);
124
-                }
125
-            }
126
-
127
-            $file_info['type'] = $session['type'];
128
-        }
129
-        else if (!empty($uri)) {
130
-            // To prevent from creating new sessions for the same file+user
131
-            // (e.g. when user uses F5 to refresh the page), we check first
132
-            // if such a session exist and continue with it
133
-            $db = $this->rc->get_dbh();
134
-
135
-            $res = $db->query("SELECT `id` FROM `{$this->sessions_table}`"
136
-                . " WHERE `owner` = ? AND `uri` = ? AND `readonly` = ?",
137
-                $this->user, $uri, intval($readonly));
138
-
139
-            if ($row = $db->fetch_assoc($res)) {
140
-                $session_id = $row['id'];
141
-                $res = true;
142
-            }
143
-            else if (!$db->is_error($res)) {
144
-                $session_id = rcube_utils::bin2ascii(md5(time() . $uri, true));
145
-                $owner      = $this->user;
146
-                $data       = array('origin' => $this->get_origin());
147
-
148
-                // store some file data, they will be used
149
-                // by invited users that has no access to the storage
150
-                foreach ($this->file_meta_items as $item) {
151
-                    if (isset($file_info[$item])) {
152
-                        $data[$item] = $file_info[$item];
153
-                    }
154
-                }
155
-
156
-                // bind the session ID with editor type (see file_document::get_handler())
157
-                if ($this instanceof file_wopi) {
158
-                    $session_id = 'w-' . $session_id;
159
-                }
160
-
161
-                // we'll store user credentials if the file comes from
162
-                // an external source that requires authentication
163
-                if ($backend != $driver) {
164
-                    $auth = $driver->auth_info();
165
-                    $auth['password']  = $this->rc->encrypt($auth['password']);
166
-                    $data['auth_info'] = $auth;
167
-                }
168
-
169
-                $res = $this->session_create($session_id, $uri, $owner, $data, $readonly);
170
-            }
171
-
172
-            if (!$res) {
173
-                throw new Exception("Failed creating document editing session", file_api_core::ERROR_CODE);
174
-            }
175
-        }
176
-        else {
177
-            throw new Exception("Failed creating document editing session (unknown file)", file_api_core::ERROR_CODE);
178
-        }
179
-
180
-        // Implementations should return real URI
181
-        return '';
182
-    }
183
-
184
-    /**
185
-     * Get file path (not URI) from session.
186
-     *
187
-     * @param string $id        Session ID
188
-     * @param bool   $join_mode Throw exception only if session does not exist
189
-     *
190
-     * @return array File info (file, type, size)
191
-     * @throws Exception
192
-     */
193
-    public function session_file($id, $join_mode = false)
194
-    {
195
-        $session = $this->session_info($id);
196
-
197
-        if (empty($session)) {
198
-            throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
199
-        }
200
-
201
-        $path = $this->uri2path($session['uri']);
202
-
203
-        if (empty($path) && (!$join_mode || $session['owner'] == $this->user)) {
204
-            throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
205
-        }
206
-
207
-        // check permissions to the session
208
-        if ($session['owner'] != $this->user) {
209
-            $invitations = $this->invitations_find(array('session_id' => $id, 'user' => $this->user));
210
-            $states      = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
211
-
212
-            if (empty($invitations) || !in_array($invitations[0]['status'], $states)) {
213
-                throw new Exception("No permission to join the editing session.", file_api_core::ERROR_CODE);
214
-            }
215
-        }
216
-
217
-        $result = array('file' => $path);
218
-
219
-        foreach ($this->file_meta_items as $item) {
220
-            if (isset($session[$item])) {
221
-                $result[$item] = $session[$item];
222
-            }
223
-        }
224
-
225
-        return $result;
226
-    }
227
-
228
-    /**
229
-     * Get editing session info
230
-     *
231
-     * @param string $id               Session identifier
232
-     * @param bool   $with_invitations Return invitations list
233
-     *
234
-     * @return array Session data
235
-     */
236
-    public function session_info($id, $with_invitations = false)
237
-    {
238
-        $session = $this->icache["session:$id"];
239
-
240
-        if (!$session) {
241
-            $db     = $this->rc->get_dbh();
242
-            $result = $db->query("SELECT * FROM `{$this->sessions_table}`"
243
-                . " WHERE `id` = ?", $id);
244
-
245
-            if ($row = $db->fetch_assoc($result)) {
246
-                $session = $this->session_info_parse($row);
247
-
248
-                $this->icache["session:$id"] = $session;
249
-            }
250
-        }
251
-
252
-        if ($session) {
253
-            if ($session['owner'] == $this->user) {
254
-                $session['is_owner'] = true;
255
-            }
256
-
257
-            if ($with_invitations && $session['is_owner']) {
258
-                $session['invitations'] = $this->invitations_find(array('session_id' => $id));
259
-            }
260
-        }
261
-
262
-        return $session;
263
-    }
264
-
265
-    /**
266
-     * Find editing sessions for specified path
267
-     */
268
-    public function session_find($path, $invitations = true)
269
-    {
270
-        // create an URI for specified path
271
-        $uri = trim($this->path2uri($path), '/') . '/';
272
-
273
-        // get existing sessions
274
-        $sessions = array();
275
-        $filter   = array('file', 'owner', 'owner_name', 'is_owner');
276
-        $db       = $this->rc->get_dbh();
277
-        $result   = $db->query("SELECT * FROM `{$this->sessions_table}`"
278
-            . " WHERE `readonly` = 0 AND `uri` LIKE '" . $db->escape($uri) . "%'");
279
-
280
-        while ($row = $db->fetch_assoc($result)) {
281
-            if ($path = $this->uri2path($row['uri'])) {
282
-                $sessions[$row['id']] = $this->session_info_parse($row, $path, $filter);
283
-            }
284
-        }
285
-
286
-        // set 'is_invited' flag
287
-        if ($invitations && !empty($sessions)) {
288
-            $invitations = $this->invitations_find(array('user' => $this->user));
289
-            $states      = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
290
-
291
-            foreach ($invitations as $invitation) {
292
-                if (!empty($sessions[$invitation['session_id']]) && in_array($invitation['status'], $states)) {
293
-                    $sessions[$invitation['session_id']]['is_invited'] = true;
294
-                }
295
-            }
296
-        }
297
-
298
-        return $sessions;
299
-    }
300
-
301
-    /**
302
-     * Delete editing session (only owner can do that)
303
-     *
304
-     * @param string $id Session identifier
305
-     */
306
-    public function session_delete($id)
307
-    {
308
-        $db     = $this->rc->get_dbh();
309
-        $result = $db->query("DELETE FROM `{$this->sessions_table}`"
310
-            . " WHERE `id` = ? AND `owner` = ?",
311
-            $id, $this->user);
312
-
313
-        return $db->affected_rows($result) > 0;
314
-    }
315
-
316
-    /**
317
-     * Update editing session
318
-     *
319
-     * @param string $id   Session ID
320
-     * @param array  $data Session metadata
321
-     */
322
-    public function session_update($id, $data)
323
-    {
324
-       $db     = $this->rc->get_dbh();
325
-       $result = $db->query("SELECT `data` FROM `{$this->sessions_table}`"
326
-           . " WHERE `id` = ?", $id);
327
-
328
-       if ($row = $db->fetch_assoc($result)) {
329
-            // merge only relevant information
330
-            $data = array_intersect_key($data, array_flip($this->file_meta_items));
331
-            if (empty($data)) {
332
-                return true;
333
-            }
334
-
335
-            $sess_data = json_decode($row['data'], true);
336
-            $sess_data = array_merge($sess_data, $data);
337
-
338
-            $result = $db->query("UPDATE `{$this->sessions_table}`"
339
-                . " SET `data` = ? WHERE `id` = ?",
340
-                json_encode($sess_data), $id);
341
-
342
-            return $db->affected_rows($result) > 0;
343
-        }
344
-
345
-        return false;
346
-    }
347
-
348
-    /**
349
-     * Create editing session
350
-     */
351
-    protected function session_create($id, $uri, $owner, $data, $readonly = false)
352
-    {
353
-        // get user name
354
-        $owner_name = $this->api->resolve_user($owner) ?: '';
355
-
356
-        $db     = $this->rc->get_dbh();
357
-        $result = $db->query("INSERT INTO `{$this->sessions_table}`"
358
-            . " (`id`, `uri`, `owner`, `owner_name`, `data`, `readonly`)"
359
-            . " VALUES (?, ?, ?, ?, ?, ?)",
360
-            $id, $uri, $owner, $owner_name, json_encode($data), intval($readonly));
361
-
362
-        return $db->affected_rows($result) > 0;
363
-    }
364
-
365
-    /**
366
-     * Find sessions, including:
367
-     *   1. to which the user has access (is a creator or has been invited)
368
-     *   2. to which the user is considered eligible to request authorization
369
-     *     to participate in the session by already having access to the file
370
-     * Note: Readonly sessions are ignored here.
371
-     *
372
-     * @param array $param List parameters
373
-     *
374
-     * @return array Sessions list
375
-     */
376
-    public function sessions_list($param = array())
377
-    {
378
-        $db       = $this->rc->get_dbh();
379
-        $sessions = array();
380
-
381
-        // 1. Get sessions user has access to
382
-        $result = $db->query("SELECT * FROM `{$this->sessions_table}` s"
383
-            . " WHERE s.`readonly` = 0 AND (s.`owner` = ? OR s.`id` IN ("
384
-                . "SELECT i.`session_id` FROM `{$this->invitations_table}` i"
385
-                . " WHERE i.`user` = ?"
386
-            . "))",
387
-            $this->user, $this->user);
388
-
389
-        if ($db->is_error($result)) {
390
-            throw new Exception("Internal error.", file_api_core::ERROR_CODE);
391
-        }
392
-
393
-        while ($row = $db->fetch_assoc($result)) {
394
-            if ($path = $this->uri2path($row['uri'], true)) {
395
-                $sessions[$row['id']] = $this->session_info_parse($row, $path);
396
-            }
397
-        }
398
-
399
-        // 2. Get sessions user is eligible
400
-        // - get list of all folder URIs and find sessions for files in these locations
401
-        // @FIXME: in corner cases (user has many folders) this may produce a big query,
402
-        // maybe fetching all sessions and then comparing with list of locations would be faster?
403
-        $uris = $this->all_folder_locations();
404
-
405
-        if (!empty($uris)) {
406
-            $where = array_map(function($uri) use ($db) {
407
-                    return 's.`uri` LIKE ' . $db->quote(str_replace('%', '_', $uri) . '/%');
408
-                }, $uris);
409
-
410
-            $result = $db->query("SELECT * FROM `{$this->sessions_table}` s"
411
-                . " WHERE s.`readonly` = 0 AND (" . join(' OR ', $where) . ")");
412
-
413
-            if ($db->is_error($result)) {
414
-                throw new Exception("Internal error.", file_api_core::ERROR_CODE);
415
-            }
416
-
417
-            while ($row = $db->fetch_assoc($result)) {
418
-                if (empty($sessions[$row['id']])) {
419
-                    // remove filename (and anything after it) so we have the folder URI
420
-                    // to check if it's on the folders list we have
421
-                    $uri = substr($row['uri'], 0, strrpos($row['uri'], '/'));
422
-                    if (in_array($uri, $uris) && ($path = $this->uri2path($row['uri'], true))) {
423
-                        $sessions[$row['id']] = $this->session_info_parse($row, $path);
424
-                    }
425
-                }
426
-            }
427
-        }
428
-
429
-        // set 'is_invited' flag
430
-        if (!empty($sessions)) {
431
-            $invitations = $this->invitations_find(array('user' => $this->user));
432
-            $states      = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
433
-
434
-            foreach ($invitations as $invitation) {
435
-                if (!empty($sessions[$invitation['session_id']]) && in_array($invitation['status'], $states)) {
436
-                    $sessions[$invitation['session_id']]['is_invited'] = true;
437
-                }
438
-            }
439
-        }
440
-
441
-        // Sorting
442
-        $sort  = !empty($params['sort']) ? $params['sort'] : 'name';
443
-        $index = array();
444
-
445
-        if (in_array($sort, array('name', 'file', 'owner'))) {
446
-            foreach ($sessions as $key => $val) {
447
-                if ($sort == 'name' || $sort == 'file') {
448
-                    $path        = explode(file_storage::SEPARATOR, $val['file']);
449
-                    $index[$key] = $path[count($path) - 1];
450
-                    continue;
451
-                }
452
-
453
-                $index[$key] = $val[$sort];
454
-            }
455
-            array_multisort($index, SORT_ASC, SORT_LOCALE_STRING, $sessions);
456
-        }
457
-
458
-        if ($params['reverse']) {
459
-            $sessions = array_reverse($sessions, true);
460
-        }
461
-
462
-        return $sessions;
463
-    }
464
-
465
-    /**
466
-     * Retern extra editor parameters to post the the viewer iframe
467
-     *
468
-     * @param array $info File info
469
-     *
470
-     * @return array POST parameters
471
-     */
472
-    public function editor_post_params($info)
473
-    {
474
-        return array();
475
-    }
476
-
477
-    /**
478
-     * Find invitations for current user. This will return all
479
-     * invitations related to the user including his sessions.
480
-     *
481
-     * @param array $filter Search filter (see self::invitations_find())
482
-     *
483
-     * @return array Invitations list
484
-     */
485
-    public function invitations_list($filter = array())
486
-    {
487
-        $filter['user'] = $this->user;
488
-
489
-        // list of invitations to the user or requested by him
490
-        $result = $this->invitations_find($filter, true);
491
-
492
-        unset($filter['user']);
493
-        $filter['owner'] = $this->user;
494
-
495
-        // other invitations that belong to the sessions owned by the user
496
-        if ($other = $this->invitations_find($filter, true)) {
497
-            $result = array_merge($result, $other);
498
-        }
499
-
500
-        return $result;
501
-    }
502
-
503
-    /**
504
-     * Find invitations for specified filter
505
-     *
506
-     * @param array $filter Search filter (see self::invitations_find())
507
-     *                      - session_id: session identifier
508
-     *                      - timestamp: "changed > ?" filter
509
-     *                      - user: Invitation user identifier
510
-     *                      - owner: Session owner identifier
511
-     * @param bool $extended Return session file names
512
-     *
513
-     * @return array Invitations list
514
-     */
515
-    public function invitations_find($filter, $extended = false)
516
-    {
517
-        $db     = $this->rc->get_dbh();
518
-        $query  = '';
519
-        $select = "i.*";
520
-
521
-        foreach ($filter as $column => $value) {
522
-            if ($column == 'timestamp') {
523
-                $where[] = "i.`changed` > " . $db->quote($this->db_datetime($value));
524
-            }
525
-            else if ($column == 'owner') {
526
-                $join[] = "`{$this->sessions_table}` s ON (i.`session_id` = s.`id`)";
527
-                $where[] = "s.`owner` = " . $db->quote($value);
528
-            }
529
-            else {
530
-                $where[] = "i.`$column` = " . $db->quote($value);
531
-            }
532
-        }
533
-
534
-        if ($extended) {
535
-            $select .= ", s.`uri`, s.`owner`, s.`owner_name`";
536
-            $join[]  = "`{$this->sessions_table}` s ON (i.`session_id` = s.`id`)";
537
-        }
538
-
539
-        if (!empty($join)) {
540
-            $query .= ' JOIN ' . implode(' JOIN ', array_unique($join));
541
-        }
542
-
543
-        if (!empty($where)) {
544
-            $query .= ' WHERE ' . implode(' AND ', array_unique($where));
545
-        }
546
-
547
-        $result = $db->query("SELECT $select FROM `{$this->invitations_table}` i"
548
-            . "$query ORDER BY i.`changed`");
549
-
550
-        if ($db->is_error($result)) {
551
-            throw new Exception("Internal error.", file_api_core::ERROR_CODE);
552
-        }
553
-
554
-        $invitations = array();
555
-
556
-        while ($row = $db->fetch_assoc($result)) {
557
-            if ($extended) {
558
-                try {
559
-                    // add unix-timestamp of the `changed` date to the result
560
-                    $dt = new DateTime($row['changed']);
561
-                    $row['timestamp'] = $dt->format('U');
562
-                }
563
-                catch(Exception $e) { }
564
-
565
-                // add filename to the result
566
-                $filename = parse_url($row['uri'], PHP_URL_PATH);
567
-                $filename = pathinfo($filename, PATHINFO_BASENAME);
568
-                $filename = rawurldecode($filename);
569
-
570
-                $row['filename'] = $filename;
571
-
572
-                if ($path = $this->uri2path($row['uri'])) {
573
-                    $row['file'] = $path;
574
-                }
575
-
576
-                unset($row['uri']);
577
-            }
578
-
579
-            $invitations[] = $row;
580
-        }
581
-
582
-        return $invitations;
583
-    }
584
-
585
-    /**
586
-     * Create an invitation
587
-     *
588
-     * @param string $session_id Document session identifier
589
-     * @param string $user       User identifier (use null for current user)
590
-     * @param string $status     Invitation status (invited, requested)
591
-     * @param string $comment    Invitation description/comment
592
-     * @param string &$user_name Optional user name
593
-     *
594
-     * @throws Exception
595
-     */
596
-    public function invitation_create($session_id, $user, $status = 'invited', $comment = '', &$user_name = '')
597
-    {
598
-        if (empty($user)) {
599
-            $user = $this->user;
600
-        }
601
-
602
-        if ($status != self::STATUS_INVITED && $status != self::STATUS_REQUESTED) {
603
-            throw new Exception("Invalid invitation status.", file_api_core::ERROR_CODE);
604
-        }
605
-
606
-        // get session information
607
-        $session = $this->session_info($session_id);
608
-
609
-        if (empty($session)) {
610
-            throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
611
-        }
612
-
613
-        // check session ownership, only owner can create 'new' invitations
614
-        if ($status == self::STATUS_INVITED && $session['owner'] != $this->user) {
615
-            throw new Exception("No permission to create an invitation.", file_api_core::ERROR_CODE);
616
-        }
617
-
618
-        if ($session['owner'] == $user) {
619
-            throw new Exception("Not possible to create an invitation for the session creator.", file_api_core::ERROR_CODE);
620
-        }
621
-
622
-        // get user name
623
-        if (empty($user_name)) {
624
-            $user_name = $this->api->resolve_user($user) ?: '';
625
-        }
626
-
627
-        // insert invitation
628
-        $db     = $this->rc->get_dbh();
629
-        $result = $db->query("INSERT INTO `{$this->invitations_table}`"
630
-            . " (`session_id`, `user`, `user_name`, `status`, `comment`, `changed`)"
631
-            . " VALUES (?, ?, ?, ?, ?, ?)",
632
-            $session_id, $user, $user_name, $status, $comment ?: '', $this->db_datetime());
633
-
634
-        if (!$db->affected_rows($result)) {
635
-            throw new Exception("Failed to create an invitation.", file_api_core::ERROR_CODE);
636
-        }
637
-    }
638
-
639
-    /**
640
-     * Delete an invitation (only session owner can do that)
641
-     *
642
-     * @param string $session_id Session identifier
643
-     * @param string $user       User identifier
644
-     * @param bool   $local      Remove invitation only from local database
645
-     *
646
-     * @throws Exception
647
-     */
648
-    public function invitation_delete($session_id, $user, $local = false)
649
-    {
650
-        $db     = $this->rc->get_dbh();
651
-        $result = $db->query("DELETE FROM `{$this->invitations_table}`"
652
-            . " WHERE `session_id` = ? AND `user` = ?"
653
-                . " AND EXISTS (SELECT 1 FROM `{$this->sessions_table}` WHERE `id` = ? AND `owner` = ?)",
654
-            $session_id, $user, $session_id, $this->user);
655
-
656
-        if (!$db->affected_rows($result)) {
657
-            throw new Exception("Failed to delete an invitation.", file_api_core::ERROR_CODE);
658
-        }
659
-    }
660
-
661
-    /**
662
-     * Update an invitation status
663
-     *
664
-     * @param string $session_id Session identifier
665
-     * @param string $user       User identifier (use null for current user)
666
-     * @param string $status     Invitation status (accepted, declined)
667
-     * @param string $comment    Invitation description/comment
668
-     *
669
-     * @throws Exception
670
-     */
671
-    public function invitation_update($session_id, $user, $status, $comment = '')
672
-    {
673
-        if (empty($user)) {
674
-            $user = $this->user;
675
-        }
676
-
677
-        if ($status != self::STATUS_ACCEPTED && $status != self::STATUS_DECLINED) {
678
-            throw new Exception("Invalid invitation status.", file_api_core::ERROR_CODE);
679
-        }
680
-
681
-        // get session information
682
-        $session = $this->session_info($session_id);
683
-
684
-        if (empty($session)) {
685
-            throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
686
-        }
687
-
688
-        // check session ownership
689
-        if ($user != $this->user && $session['owner'] != $this->user) {
690
-            throw new Exception("No permission to update an invitation.", file_api_core::ERROR_CODE);
691
-        }
692
-
693
-        if ($session['owner'] == $this->user) {
694
-            $status = $status . '-owner';
695
-        }
696
-
697
-        $db     = $this->rc->get_dbh();
698
-        $result = $db->query("UPDATE `{$this->invitations_table}`"
699
-            . " SET `status` = ?, `comment` = ?, `changed` = ?"
700
-            . " WHERE `session_id` = ? AND `user` = ?",
701
-            $status, $comment ?: '', $this->db_datetime(), $session_id, $user);
702
-
703
-        if (!$db->affected_rows($result)) {
704
-            throw new Exception("Failed to update an invitation status.", file_api_core::ERROR_CODE);
705
-        }
706
-    }
707
-
708
-    /**
709
-     * Update a session URI (e.g. on file/folder move)
710
-     *
711
-     * @param string $from      Source file/folder path
712
-     * @param string $to        Destination file/folder path
713
-     * @param bool   $is_folder True if the path is a folder
714
-     */
715
-    public function session_uri_update($from, $to, $is_folder = false)
716
-    {
717
-        $db = $this->rc->get_dbh();
718
-
719
-        // Resolve paths
720
-        $from = $this->path2uri($from);
721
-        $to   = $this->path2uri($to);
722
-
723
-        if ($is_folder) {
724
-            $set = "`uri` = REPLACE(`uri`, " . $db->quote($from . '/') . ", " . $db->quote($to .'/') . ")";
725
-            $where = "`uri` LIKE " . $db->quote(str_replace('%', '_', $from) . '/%');
726
-        }
727
-        else {
728
-            $set = "`uri` = " . $db->quote($to);
729
-            $where = "`uri` = " . $db->quote($from);
730
-        }
731
-
732
-        $db->query("UPDATE `{$this->sessions_table}` SET $set WHERE $where");
733
-    }
734
-
735
-    /**
736
-     * Parse session info data
737
-     */
738
-    protected function session_info_parse($record, $path = null, $filter = array())
739
-    {
740
-        $session = array();
741
-        $fields  = array('id', 'uri', 'owner', 'owner_name', 'readonly');
742
-
743
-        foreach ($fields as $field) {
744
-            if (isset($record[$field])) {
745
-                $session[$field] = $record[$field];
746
-            }
747
-        }
748
-
749
-        if ($path) {
750
-            $session['file'] = $path;
751
-        }
752
-
753
-        if (!empty($record['data'])) {
754
-            $data   = json_decode($record['data'], true);
755
-            $fields = array_merge($this->file_meta_items, array('origin'));
756
-
757
-            foreach ($fields as $field) {
758
-                if (empty($filter) || in_array($field, $filter)) {
759
-                    $session[$field] = $data[$field];
760
-                }
761
-            }
762
-        }
763
-
764
-        // @TODO: is_invited?, last_modified?
765
-
766
-        if ($session['owner'] == $this->user) {
767
-            $session['is_owner'] = true;
768
-        }
769
-
770
-        if (!empty($filter)) {
771
-            $session = array_intersect_key($session, array_flip($filter));
772
-        }
773
-
774
-        return $session;
775
-    }
776
-
777
-    /**
778
-     * Get file URI from path
779
-     */
780
-    protected function path2uri($path, &$driver = null)
781
-    {
782
-        list($driver, $path) = $this->api->get_driver($path);
783
-
784
-        return $driver->path2uri($path);
785
-    }
786
-
787
-    /**
788
-     * Get file path from the URI
789
-     */
790
-    protected function uri2path($uri, $use_fallback = false)
791
-    {
792
-        $backend = $this->api->get_backend();
793
-
794
-        try {
795
-            return $backend->uri2path($uri);
796
-        }
797
-        catch (Exception $e) {
798
-                // do nothing
799
-        }
800
-
801
-        foreach ($this->api->get_drivers(true) as $driver) {
802
-            try {
803
-                $path  = $driver->uri2path($uri);
804
-                $title = $driver->title();
805
-
806
-                if ($title) {
807
-                    $path = $title . file_storage::SEPARATOR . $path;
808
-                }
809
-
810
-                return $path;
811
-            }
812
-            catch (Exception $e) {
813
-                // do nothing
814
-            }
815
-        }
816
-
817
-        // likely user has no access to the file, but has been invited,
818
-        // extract filename from the URI
819
-        if ($use_fallback && $uri) {
820
-            $path = parse_url($uri, PHP_URL_PATH);
821
-            $path = explode('/', $path);
822
-            $path = $path[count($path) - 1];
823
-
824
-            return $path;
825
-        }
826
-    }
827
-
828
-    /**
829
-     * Get URI of all user folders (with shared locations)
830
-     */
831
-    protected function all_folder_locations()
832
-    {
833
-        $locations = array();
834
-
835
-        foreach (array_merge(array($this->api->get_backend()), $this->api->get_drivers(true)) as $driver) {
836
-            // Performance optimization: We're interested here in shared folders,
837
-            // Kolab is the only driver that currently supports them, ignore others
838
-            if (get_class($driver) != 'kolab_file_storage') {
839
-                continue;
840
-            }
841
-
842
-            try {
843
-                foreach ($driver->folder_list() as $folder) {
844
-                    if ($uri = $driver->path2uri($folder)) {
845
-                        $locations[] = $uri;
846
-                    }
847
-                }
848
-            }
849
-            catch (Exception $e) {
850
-               // do nothing
851
-            }
852
-        }
853
-
854
-        return $locations;
855
-    }
856
-
857
-    /**
858
-     * Get request origin, use Referer header if specified
859
-     */
860
-    protected function get_origin()
861
-    {
862
-        if (!empty($_SERVER['HTTP_REFERER'])) {
863
-            $url = parse_url($_SERVER['HTTP_REFERER']);
864
-
865
-            return $url['scheme'] . '://' . $url['host'] . ($url['port'] ?: '');
866
-        }
867
-
868
-        return (rcube_utils::https_check() ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
869
-    }
870
-
871
-    /**
872
-     * Return datetime in UTC timezone in SQL format
873
-     */
874
-    protected function db_datetime($dt = null)
875
-    {
876
-        $timezone = new DateTimeZone('UTC');
877
-        $datetime = new DateTime($dt ? '@'.$dt : 'now', $timezone);
878
-
879
-        return $datetime->format(self::DB_DATE_FORMAT);
880
-    }
881
-}
882
iRony-0.4.3.tar.gz/lib/FileAPI/file_manticore.php Deleted
240
 
1
@@ -1,238 +0,0 @@
2
-<?php
3
-/**
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2016, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-/**
27
- * Document editing sessions handling (Manticore)
28
- */
29
-class file_manticore extends file_document
30
-{
31
-    protected $request;
32
-
33
-
34
-    /**
35
-     * Return viewer URI for specified file/session. This creates
36
-     * a new collaborative editing session when needed.
37
-     *
38
-     * @param string $file        File path
39
-     * @param array  &$file_info  File metadata (e.g. type)
40
-     * @param string &$session_id Optional session ID to join to
41
-     * @param string $readonly    Create readonly (one-time) session
42
-     *
43
-     * @return string Manticore URI
44
-     * @throws Exception
45
-     */
46
-    public function session_start($file, &$file_info, &$session_id = null, $readonly = false)
47
-    {
48
-        parent::session_start($file, $file_info, $session_id, $readonly);
49
-
50
-        // authenticate to Manticore, we need auth token for frame_uri
51
-        if (empty($_SESSION['manticore_token'])) {
52
-            $this->get_request();
53
-        }
54
-
55
-        // @TODO: make sure the session exists in Manticore?
56
-
57
-        return $this->frame_uri($session_id);
58
-    }
59
-
60
-    /**
61
-     * Delete editing session (only owner can do that)
62
-     *
63
-     * @param string $id    Session identifier
64
-     * @param bool   $local Remove session only from local database
65
-     */
66
-    public function session_delete($id, $local = false)
67
-    {
68
-        $success = parent::session_delete($id, $local);
69
-
70
-        // Send document delete to Manticore
71
-        if ($success && !$local) {
72
-            $req = $this->get_request();
73
-            $res = $req->document_delete($id);
74
-        }
75
-
76
-        return $success;
77
-    }
78
-
79
-    /**
80
-     * Create editing session
81
-     */
82
-    protected function session_create($id, $uri, $owner, $data, $readonly = false)
83
-    {
84
-        $success = parent::session_create($id, $uri, $owner, $data, $readonly);
85
-
86
-        // create the session in Manticore
87
-        if ($success) {
88
-            $req = $this->get_request();
89
-            $res = $req->document_create(array(
90
-                'id'     => $id,
91
-                'title'  => '', // @TODO: maybe set to a file path without extension?
92
-                'access' => array(
93
-                    array(
94
-                        'identity'   => $owner,
95
-                        'permission' => file_manticore_api::ACCESS_WRITE,
96
-                    ),
97
-                ),
98
-            ));
99
-
100
-            if (!$res) {
101
-                $this->session_delete($id, true);
102
-                return false;
103
-            }
104
-        }
105
-
106
-        return $success;
107
-    }
108
-
109
-    /**
110
-     * Create an invitation
111
-     *
112
-     * @param string $session_id Document session identifier
113
-     * @param string $user       User identifier (use null for current user)
114
-     * @param string $status     Invitation status (invited, requested)
115
-     * @param string $comment    Invitation description/comment
116
-     * @param string &$user_name Optional user name
117
-     *
118
-     * @throws Exception
119
-     */
120
-    public function invitation_create($session_id, $user, $status = 'invited', $comment = '', &$user_name = '')
121
-    {
122
-        parent::invitation_create($session_id, $user, $status, $comment, $user_name);
123
-
124
-        // Update Manticore 'access' array
125
-        if ($status == file_document::STATUS_INVITED) {
126
-            $req = $this->get_request();
127
-            $res = $req->editor_add($session_id, $user, file_manticore_api::ACCESS_WRITE);
128
-
129
-            if (!$res) {
130
-                $this->invitation_delete($session_id, $user, true);
131
-                throw new Exception("Failed to create an invitation.", file_api_core::ERROR_CODE);
132
-            }
133
-        }
134
-    }
135
-
136
-    /**
137
-     * Delete an invitation (only session owner can do that)
138
-     *
139
-     * @param string $session_id Session identifier
140
-     * @param string $user       User identifier
141
-     * @param bool   $local      Remove invitation only from local database
142
-     *
143
-     * @throws Exception
144
-     */
145
-    public function invitation_delete($session_id, $user, $local = false)
146
-    {
147
-        parent::invitation_delete($session_id, $user, $local);
148
-
149
-        // Update Manticore 'access' array
150
-        if (!$local) {
151
-            $req = $this->get_request();
152
-            $res = $req->editor_delete($session_id, $user);
153
-
154
-            if (!$res) {
155
-                throw new Exception("Failed to remove an invitation.", file_api_core::ERROR_CODE);
156
-            }
157
-        }
158
-    }
159
-
160
-    /**
161
-     * Update an invitation status
162
-     *
163
-     * @param string $session_id Session identifier
164
-     * @param string $user       User identifier (use null for current user)
165
-     * @param string $status     Invitation status (accepted, declined)
166
-     * @param string $comment    Invitation description/comment
167
-     *
168
-     * @throws Exception
169
-     */
170
-    public function invitation_update($session_id, $user, $status, $comment = '')
171
-    {
172
-        parent::invitation_update($session_id, $user, $status, $comment);
173
-
174
-        // Update Manticore 'access' array if an owner accepted an invitation request
175
-        if ($status == file_document::STATUS_ACCEPTED_OWNER) {
176
-            $req = $this->get_request();
177
-            $res = $req->editor_add($session_id, $user, file_manticore_api::ACCESS_WRITE);
178
-
179
-            if (!$res) {
180
-                throw new Exception("Failed to update an invitation status.", file_api_core::ERROR_CODE);
181
-            }
182
-        }
183
-    }
184
-
185
-    /**
186
-     * List supported mimetypes
187
-     *
188
-     * @param bool $editable Return only editable mimetypes
189
-     *
190
-     * @return array List of supported mimetypes
191
-     */
192
-    public function supported_filetypes($editable = false)
193
-    {
194
-        return array(
195
-            'application/vnd.oasis.opendocument.text',
196
-        );
197
-    }
198
-
199
-    /**
200
-     * Generate URI of Manticore editing session
201
-     */
202
-    protected function frame_uri($id)
203
-    {
204
-        $base_url = rtrim($this->rc->config->get('fileapi_manticore'), ' /');
205
-
206
-        return $base_url . '/document/' . $id . '/' . $_SESSION['manticore_token'];
207
-    }
208
-
209
-    /**
210
-     * Initialize Manticore API request handler
211
-     */
212
-    protected function get_request()
213
-    {
214
-        if (!$this->request) {
215
-            $uri = rcube_utils::resolve_url($this->rc->config->get('fileapi_manticore'));
216
-            $this->request = new file_manticore_api($uri);
217
-
218
-            // Use stored session token, check if it's still valid
219
-            if ($_SESSION['manticore_token']) {
220
-                $is_valid = $this->request->set_session_token($_SESSION['manticore_token'], true);
221
-
222
-                if ($is_valid) {
223
-                    return $this->request;
224
-                }
225
-            }
226
-
227
-            $backend = $this->api->get_backend();
228
-            $auth    = $backend->auth_info();
229
-
230
-            $_SESSION['manticore_token'] = $this->request->login($auth['username'], $auth['password']);
231
-
232
-            if (empty($_SESSION['manticore_token'])) {
233
-                throw new Exception("Unable to login to Manticore server.", file_api_core::ERROR_CODE);
234
-            }
235
-        }
236
-
237
-        return $this->request;
238
-    }
239
-}
240
iRony-0.4.3.tar.gz/lib/FileAPI/file_manticore_api.php Deleted
442
 
1
@@ -1,440 +0,0 @@
2
-<?php
3
-/**
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2011-2015, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-/**
27
- * Helper class to connect to the Manticore API
28
- */
29
-class file_manticore_api
30
-{
31
-    /**
32
-     * @var HTTP_Request2
33
-     */
34
-    private $request;
35
-
36
-    /**
37
-     * @var string
38
-     */
39
-    private $base_url;
40
-
41
-    /**
42
-     * @var bool
43
-     */
44
-    private $debug = false;
45
-
46
-    const ERROR_INTERNAL   = 100;
47
-    const ERROR_CONNECTION = 500;
48
-
49
-    const ACCEPT_HEADER = "application/json,text/javascript,*/*";
50
-
51
-    const ACCESS_WRITE = 'write';
52
-    const ACCESS_READ  = 'read';
53
-    const ACCESS_DENY  = 'deny';
54
-
55
-
56
-    /**
57
-     * Class constructor.
58
-     *
59
-     * @param string $base_url Base URL of the Kolab API
60
-     */
61
-    public function __construct($base_url)
62
-    {
63
-        require_once 'HTTP/Request2.php';
64
-
65
-        $config         = rcube::get_instance()->config;
66
-        $this->debug    = rcube_utils::get_boolean($config->get('fileapi_manticore_debug'));
67
-        $this->base_url = rtrim($base_url, '/') . '/';
68
-        $this->request  = new HTTP_Request2();
69
-
70
-        self::configure($this->request);
71
-    }
72
-
73
-    /**
74
-     * Configure HTTP_Request2 object
75
-     *
76
-     * @param HTTP_Request2 $request Request object
77
-     */
78
-    public static function configure($request)
79
-    {
80
-        // Configure connection options
81
-        $config      = rcube::get_instance()->config;
82
-        $http_config = (array) $config->get('http_request', $config->get('kolab_http_request'));
83
-
84
-        // Deprecated config, all options are separated variables
85
-        if (empty($http_config)) {
86
-            $options = array(
87
-                'ssl_verify_peer',
88
-                'ssl_verify_host',
89
-                'ssl_cafile',
90
-                'ssl_capath',
91
-                'ssl_local_cert',
92
-                'ssl_passphrase',
93
-                'follow_redirects',
94
-            );
95
-
96
-            foreach ($options as $optname) {
97
-                if (($optvalue = $config->get($optname)) !== null
98
-                    || ($optvalue = $config->get('kolab_' . $optname)) !== null
99
-                ) {
100
-                    $http_config[$optname] = $optvalue;
101
-                }
102
-            }
103
-        }
104
-
105
-        if (!empty($http_config)) {
106
-            try {
107
-                $request->setConfig($http_config);
108
-            }
109
-            catch (Exception $e) {
110
-                rcube::log_error("HTTP: " . $e->getMessage());
111
-            }
112
-        }
113
-
114
-        // proxy User-Agent
115
-        $request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
116
-
117
-        // some HTTP server configurations require this header
118
-        $request->setHeader('accept', self::ACCEPT_HEADER);
119
-
120
-        $request->setHeader('Content-Type', 'application/json; charset=UTF-8');
121
-    }
122
-
123
-    /**
124
-     * Return API's base URL
125
-     *
126
-     * @return string Base URL
127
-     */
128
-    public function base_url()
129
-    {
130
-        return $this->base_url;
131
-    }
132
-
133
-    /**
134
-     * Return HTTP_Request2 object
135
-     *
136
-     * @return HTTP_Request2 Request object
137
-     */
138
-    public function request()
139
-    {
140
-        return $this->request;
141
-    }
142
-
143
-    /**
144
-     * Logs specified user into the API
145
-     *
146
-     * @param string $username User name
147
-     * @param string $password User password
148
-     *
149
-     * @return string Session token (on success)
150
-     */
151
-    public function login($username, $password)
152
-    {
153
-        $query = array(
154
-            'email'    => $username,
155
-            'password' => $password,
156
-        );
157
-
158
-        // remove current token if any
159
-        $this->request->setHeader('Authorization');
160
-
161
-        // authenticate the user
162
-        $response = $this->post('auth/local', $query);
163
-
164
-        if ($token = $response->get('token')) {
165
-            $this->set_session_token($token);
166
-        }
167
-
168
-        return $token;
169
-    }
170
-
171
-    /**
172
-     * Sets request session token.
173
-     *
174
-     * @param string $token    Session token.
175
-     * @param bool   $validate Enables token validatity check
176
-     *
177
-     * @return bool Token validity status
178
-     */
179
-    public function set_session_token($token, $validate = false)
180
-    {
181
-        $this->request->setHeader('Authorization', "Bearer $token");
182
-
183
-        if ($validate) {
184
-            $result = $this->get('api/users/me');
185
-
186
-            return $result->get_error_code() == 200;
187
-        }
188
-
189
-        return true;
190
-    }
191
-
192
-    /**
193
-     * Delete document editing session
194
-     *
195
-     * @param array $id Session identifier
196
-     *
197
-     * @return bool True on success, False on failure
198
-     */
199
-    public function document_delete($id)
200
-    {
201
-        $res = $this->delete('api/documents/' . $id);
202
-
203
-        return $res->get_error_code() == 204;
204
-    }
205
-
206
-    /**
207
-     * Create document editing session
208
-     *
209
-     * @param array $params Session parameters
210
-     *
211
-     * @return bool True on success, False on failure
212
-     */
213
-    public function document_create($params)
214
-    {
215
-        $res = $this->post('api/documents', $params);
216
-
217
-        // @FIXME: 422?
218
-        return $res->get_error_code() == 201 || $res->get_error_code() == 422;
219
-    }
220
-
221
-    /**
222
-     * Add document editor (update 'access' array)
223
-     *
224
-     * @param array $session_id Session identifier
225
-     * @param array $identity   User identifier
226
-     *
227
-     * @return bool True on success, False on failure
228
-     */
229
-    public function editor_add($session_id, $identity, $permission)
230
-    {
231
-        $res = $this->get("api/documents/$session_id/access");
232
-
233
-        if ($res->get_error_code() != 200) {
234
-            return false;
235
-        }
236
-
237
-        $access = $res->get();
238
-
239
-        // sanity check, this should never be empty
240
-        if (empty($access)) {
241
-            return false;
242
-        }
243
-
244
-        // add editor to the 'access' array
245
-        foreach ($access as $entry) {
246
-            if ($entry['identity'] == $identity) {
247
-                return true;
248
-            }
249
-        }
250
-
251
-        $access[] = array('identity' => $identity, 'permission' => $permission);
252
-
253
-        $res = $this->put("api/documents/$session_id/access", $access);
254
-
255
-        return $res->get_error_code() == 200;
256
-    }
257
-
258
-    /**
259
-     * Remove document editor (update 'access' array)
260
-     *
261
-     * @param array $session_id Session identifier
262
-     * @param array $identity   User identifier
263
-     *
264
-     * @return bool True on success, False on failure
265
-     */
266
-    public function editor_delete($session_id, $identity)
267
-    {
268
-        $res = $this->get("api/documents/$session_id/access");
269
-
270
-        if ($res->get_error_code() != 200) {
271
-            return false;
272
-        }
273
-
274
-        $access = $res->get();
275
-        $found  = true;
276
-
277
-        // remove editor from the 'access' array
278
-        foreach ((array) $access as $idx => $entry) {
279
-            if ($entry['identity'] == $identity) {
280
-                unset($access[$idx]);
281
-            }
282
-        }
283
-
284
-        if (!$found) {
285
-            return false;
286
-        }
287
-
288
-        $res = $this->put("api/documents/$session_id/access", $access);
289
-
290
-        return $res->get_error_code() == 200;
291
-    }
292
-
293
-    /**
294
-     * API's GET request.
295
-     *
296
-     * @param string $action Action name
297
-     * @param array  $get    Request arguments
298
-     *
299
-     * @return file_ui_api_result Response
300
-     */
301
-    public function get($action, $get = array())
302
-    {
303
-        $url = $this->build_url($action, $get);
304
-
305
-        if ($this->debug) {
306
-            rcube::write_log('manticore', "GET: $url " . json_encode($get));
307
-        }
308
-
309
-        $this->request->setMethod(HTTP_Request2::METHOD_GET);
310
-        $this->request->setBody('');
311
-
312
-        return $this->get_response($url);
313
-    }
314
-
315
-    /**
316
-     * API's POST request.
317
-     *
318
-     * @param string $action Action name
319
-     * @param array  $post   POST arguments
320
-     *
321
-     * @return kolab_client_api_result Response
322
-     */
323
-    public function post($action, $post = array())
324
-    {
325
-        $url = $this->build_url($action);
326
-
327
-        if ($this->debug) {
328
-            rcube::write_log('manticore', "POST: $url " . json_encode($post));
329
-        }
330
-
331
-        $this->request->setMethod(HTTP_Request2::METHOD_POST);
332
-        $this->request->setBody(json_encode($post));
333
-
334
-        return $this->get_response($url);
335
-    }
336
-
337
-    /**
338
-     * API's PUT request.
339
-     *
340
-     * @param string $action Action name
341
-     * @param array  $post   POST arguments
342
-     *
343
-     * @return kolab_client_api_result Response
344
-     */
345
-    public function put($action, $post = array())
346
-    {
347
-        $url = $this->build_url($action);
348
-
349
-        if ($this->debug) {
350
-            rcube::write_log('manticore', "PUT: $url " . json_encode($post));
351
-        }
352
-
353
-        $this->request->setMethod(HTTP_Request2::METHOD_PUT);
354
-        $this->request->setBody(json_encode($post));
355
-
356
-        return $this->get_response($url);
357
-    }
358
-
359
-    /**
360
-     * API's DELETE request.
361
-     *
362
-     * @param string $action Action name
363
-     * @param array  $get    Request arguments
364
-     *
365
-     * @return file_ui_api_result Response
366
-     */
367
-    public function delete($action, $get = array())
368
-    {
369
-        $url = $this->build_url($action, $get);
370
-
371
-        if ($this->debug) {
372
-            rcube::write_log('manticore', "DELETE: $url " . json_encode($get));
373
-        }
374
-
375
-        $this->request->setMethod(HTTP_Request2::METHOD_DELETE);
376
-        $this->request->setBody('');
377
-
378
-        return $this->get_response($url);
379
-    }
380
-
381
-    /**
382
-     * @param string $action Action GET parameter
383
-     * @param array  $args   GET parameters (hash array: name => value)
384
-     *
385
-     * @return Net_URL2 URL object
386
-     */
387
-    private function build_url($action, $args = array())
388
-    {
389
-        $url = new Net_URL2($this->base_url . $action);
390
-
391
-        $url->setQueryVariables((array) $args);
392
-
393
-        return $url;
394
-    }
395
-
396
-    /**
397
-     * HTTP Response handler.
398
-     *
399
-     * @param Net_URL2 $url URL object
400
-     *
401
-     * @return kolab_client_api_result Response object
402
-     */
403
-    private function get_response($url)
404
-    {
405
-        try {
406
-            $this->request->setUrl($url);
407
-            $response = $this->request->send();
408
-        }
409
-        catch (Exception $e) {
410
-            return new file_ui_api_result(null,
411
-                self::ERROR_CONNECTION, $e->getMessage());
412
-        }
413
-
414
-        try {
415
-            $body = $response->getBody();
416
-        }
417
-        catch (Exception $e) {
418
-            return new file_ui_api_result(null,
419
-                self::ERROR_INTERNAL, $e->getMessage());
420
-        }
421
-
422
-        $code = $response->getStatus();
423
-
424
-        if ($this->debug) {
425
-            rcube::write_log('manticore', "Response [$code]: $body");
426
-        }
427
-
428
-        if ($code < 300) {
429
-            $result = $body ? json_decode($body, true) : array();
430
-        }
431
-        else {
432
-            if ($code != 401) {
433
-                rcube::raise_error("Error $code on $url", true, false);
434
-            }
435
-
436
-            $error = $body;
437
-        }
438
-
439
-        return new file_ui_api_result($result, $code, $error);
440
-    }
441
-}
442
iRony-0.4.3.tar.gz/lib/FileAPI/file_wopi.php Deleted
358
 
1
@@ -1,356 +0,0 @@
2
-<?php
3
-/**
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2017, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-/**
27
- * Document editing sessions handling (WOPI)
28
- */
29
-class file_wopi extends file_document
30
-{
31
-    protected $cache;
32
-
33
-    // Mimetypes supported by CODE, but not advertised by all possible names
34
-    protected $mimetype_aliases = array(
35
-        'application/vnd.corel-draw' => 'image/x-coreldraw',
36
-    );
37
-
38
-    // Mimetypes supported by other Chwala viewers or ones we don't want to be editable
39
-    protected $mimetype_exceptions = array(
40
-        'text/plain',
41
-        'image/bmp',
42
-        'image/png',
43
-        'image/jpeg',
44
-        'image/jpg',
45
-        'image/pjpeg',
46
-        'image/gif',
47
-        'image/tiff',
48
-        'image/x-tiff',
49
-    );
50
-
51
-    /**
52
-     * Return viewer URI for specified file/session. This creates
53
-     * a new collaborative editing session when needed.
54
-     *
55
-     * @param string $file        File path
56
-     * @param array  &$file_info  File metadata (e.g. type)
57
-     * @param string &$session_id Optional session ID to join to
58
-     * @param string $readonly    Create readonly (one-time) session
59
-     *
60
-     * @return string WOPI URI for specified document
61
-     * @throws Exception
62
-     */
63
-    public function session_start($file, &$file_info, &$session_id = null, $readonly = false)
64
-    {
65
-        parent::session_start($file, $file_info, $session_id, $readonly);
66
-
67
-        if ($session_id) {
68
-            // Create Chwala session for use as WOPI access_token
69
-            // This session will have access to this one document session only
70
-            $keys = array('env', 'user_id', 'user', 'username', 'password',
71
-                'storage_host', 'storage_port', 'storage_ssl');
72
-
73
-            $data = array_intersect_key($_SESSION, array_flip($keys));
74
-            $data['document_session'] = $session_id;
75
-
76
-            $this->token = $this->api->session->create($data);
77
-
78
-            $this->log_login($session_id);
79
-        }
80
-
81
-        return $this->frame_uri($session_id, $file_info['type']);
82
-    }
83
-
84
-    /**
85
-     * Generate URI of WOPI editing session (WOPIsrc)
86
-     */
87
-    protected function frame_uri($id, $mimetype)
88
-    {
89
-        $capabilities = $this->capabilities();
90
-
91
-        if (empty($capabilities) || empty($mimetype)) {
92
-            return;
93
-        }
94
-
95
-        $metadata = $capabilities[strtolower($mimetype)];
96
-
97
-        if (empty($metadata)) {
98
-            return;
99
-        }
100
-
101
-        $office_url  = rtrim($metadata['urlsrc'], ' /?'); // collabora
102
-        $service_url = $this->api->api_url() . '/wopi/files/' . $id;
103
-
104
-        // @TODO: Parsing and replacing placeholder values
105
-        // https://wopi.readthedocs.io/en/latest/discovery.html#action-urls
106
-
107
-        $args = array('WOPISrc' => $service_url);
108
-
109
-        // We could also set: title, closebutton, revisionhistory
110
-        // @TODO: do it in editor_post_params() when supported by the editor
111
-        if ($lang = $this->api->env['language']) {
112
-            $args['lang'] = str_replace('_', '-', $lang);
113
-        }
114
-
115
-        return $office_url . '?' . http_build_query($args, '', '&');
116
-    }
117
-
118
-    /**
119
-     * Retern extra viewer parameters to post to the viewer iframe
120
-     *
121
-     * @param array $info File info
122
-     *
123
-     * @return array POST parameters
124
-     */
125
-    public function editor_post_params($info)
126
-    {
127
-        // Access token TTL (number of milliseconds since January 1, 1970 UTC)
128
-        if ($ttl = $this->rc->config->get('session_lifetime', 0) * 60) {
129
-            $now = new DateTime('now', new DateTimeZone('UTC'));
130
-            $ttl = ($ttl + $now->format('U')) . '000';
131
-        }
132
-
133
-        $params = array(
134
-            'access_token'     => $this->token,
135
-            'access_token_ttl' => $ttl ?: 0,
136
-        );
137
-
138
-        return $params;
139
-    }
140
-
141
-    /**
142
-     * List supported mimetypes
143
-     *
144
-     * @param bool $editable Return only editable mimetypes
145
-     *
146
-     * @return array List of supported mimetypes
147
-     */
148
-    public function supported_filetypes($editable = false)
149
-    {
150
-        $caps = $this->capabilities();
151
-
152
-        if ($editable) {
153
-            $editable = array();
154
-            foreach ($caps as $mimetype => $c) {
155
-                if ($c['name'] == 'edit') {
156
-                    $editable[] = $mimetype;
157
-                }
158
-            }
159
-
160
-            return $editable;
161
-        }
162
-
163
-        return array_keys($caps);
164
-    }
165
-
166
-    /**
167
-     * Uses WOPI discovery to get Office capabilities
168
-     * https://wopi.readthedocs.io/en/latest/discovery.html
169
-     */
170
-    protected function capabilities()
171
-    {
172
-        $cache_key = 'wopi.capabilities';
173
-        if ($result = $this->get_from_cache($cache_key)) {
174
-            return $this->apply_aliases_and_exceptions($result);
175
-        }
176
-
177
-        $office_url  = rtrim($this->rc->config->get('fileapi_wopi_office'), ' /');
178
-        $office_url .= '/hosting/discovery';
179
-
180
-        try {
181
-            $request = $this->http_request();
182
-            $request->setMethod(HTTP_Request2::METHOD_GET);
183
-            $request->setBody('');
184
-            $request->setUrl($office_url);
185
-
186
-            $response = $request->send();
187
-            $body     = $response->getBody();
188
-            $code     = $response->getStatus();
189
-
190
-            if (empty($body) || $code != 200) {
191
-                throw new Exception("Unexpected WOPI discovery response");
192
-            }
193
-        }
194
-        catch (Exception $e) {
195
-            rcube::raise_error($e, true, false);
196
-
197
-            // Don't bail out here, it would make the kolab_files UI broken
198
-            return array();
199
-        }
200
-
201
-        // parse XML output
202
-        // <wopi-discovery>
203
-        //   <net-zone name="external-http">
204
-        //     <app name="application/vnd.lotus-wordpro">
205
-        //       <action ext="lwp" name="edit" urlsrc="https://office.example.org/loleaflet/1.8.3/loleaflet.html?"/>
206
-        //     </app>
207
-        // ...
208
-
209
-        $node = new DOMDocument('1.0', 'UTF-8');
210
-        $node->loadXML($body);
211
-
212
-        $result = array();
213
-
214
-        foreach ($node->getElementsByTagName('app') as $app) {
215
-            if ($mimetype = $app->getAttribute('name')) {
216
-                if ($action = $app->getElementsByTagName('action')->item(0)) {
217
-                    foreach ($action->attributes as $attr) {
218
-                        $result[$mimetype][$attr->name] = $attr->value;
219
-                    }
220
-                }
221
-            }
222
-        }
223
-
224
-        if (empty($result)) {
225
-            rcube::raise_error("Failed to parse WOPI discovery response: $body", true, false);
226
-
227
-            // Don't bail out here, it would make the kolab_files UI broken
228
-            return array();
229
-        }
230
-
231
-        $this->save_in_cache($cache_key, $result);
232
-
233
-        return $this->apply_aliases_and_exceptions($result);
234
-    }
235
-
236
-    /**
237
-     * Initializes HTTP request object
238
-     */
239
-    protected function http_request()
240
-    {
241
-        require_once 'HTTP/Request2.php';
242
-
243
-        $request = new HTTP_Request2();
244
-
245
-        // Configure connection options
246
-        $config      = $this->rc->config;
247
-        $http_config = (array) $config->get('http_request', $config->get('kolab_http_request'));
248
-
249
-        // Deprecated config, all options are separated variables
250
-        if (empty($http_config)) {
251
-            $options = array(
252
-                'ssl_verify_peer',
253
-                'ssl_verify_host',
254
-                'ssl_cafile',
255
-                'ssl_capath',
256
-                'ssl_local_cert',
257
-                'ssl_passphrase',
258
-                'follow_redirects',
259
-            );
260
-
261
-            foreach ($options as $optname) {
262
-                if (($optvalue = $config->get($optname)) !== null
263
-                    || ($optvalue = $config->get('kolab_' . $optname)) !== null
264
-                ) {
265
-                    $http_config[$optname] = $optvalue;
266
-                }
267
-            }
268
-        }
269
-
270
-        if (!empty($http_config)) {
271
-            try {
272
-                $request->setConfig($http_config);
273
-            }
274
-            catch (Exception $e) {
275
-                rcube::log_error("HTTP: " . $e->getMessage());
276
-            }
277
-        }
278
-
279
-        // proxy User-Agent
280
-        $request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
281
-
282
-        // some HTTP server configurations require this header
283
-        $request->setHeader('accept', "application/json,text/javascript,*/*");
284
-
285
-        return $request;
286
-    }
287
-
288
-    /**
289
-     * Get cached data
290
-     */
291
-    protected function get_from_cache($key)
292
-    {
293
-        if ($cache = $this->get_cache()) {
294
-            return $cache->get($key);
295
-        }
296
-    }
297
-
298
-    /**
299
-     * Store data in cache
300
-     */
301
-    protected function save_in_cache($key, $value)
302
-    {
303
-        if ($cache = $this->get_cache()) {
304
-            $cache->set($key, $value);
305
-        }
306
-    }
307
-
308
-    /**
309
-     * Getter for the shared cache engine object
310
-     */
311
-    protected function get_cache()
312
-    {
313
-        if ($this->cache === null) {
314
-            $this->cache = $this->rc->get_cache_shared('fileapi') ?: false;
315
-        }
316
-
317
-        return $this->cache;
318
-    }
319
-
320
-    /**
321
-     * Support more mimetypes in CODE capabilities
322
-     */
323
-    protected function apply_aliases_and_exceptions($caps)
324
-    {
325
-        foreach ($this->mimetype_aliases as $type => $alias) {
326
-            if (isset($caps[$type]) && !isset($caps[$alias])) {
327
-                $caps[$alias] = $caps[$type];
328
-            }
329
-        }
330
-
331
-        foreach ($this->mimetype_exceptions as $type) {
332
-            unset($caps[$type]);
333
-        }
334
-
335
-        return $caps;
336
-    }
337
-
338
-    /**
339
-     * Write login data (name, ID, IP address) to the 'userlogins' log file.
340
-     */
341
-    protected function log_login($session_id)
342
-    {
343
-        if (!$this->api->config->get('log_logins')) {
344
-            return;
345
-        }
346
-
347
-        $rcube     = rcube::get_instance();
348
-        $user_name = $rcube->get_user_name();
349
-        $user_id   = $rcube->get_user_id();
350
-
351
-        $message = sprintf('CODE access for %s (ID: %d) from %s in session %s; %s',
352
-            $user_name, $user_id, rcube_utils::remote_ip(), session_id(), $session_id);
353
-
354
-        // log login
355
-        rcube::write_log('userlogins', $message);
356
-    }
357
-}
358
iRony-0.4.3.tar.gz/lib/FileAPI/templates Deleted
2
 
1
-(directory)
2
iRony-0.4.3.tar.gz/lib/FileAPI/templates/empty.odp Deleted
iRony-0.4.3.tar.gz/lib/FileAPI/templates/empty.ods Deleted
iRony-0.4.3.tar.gz/lib/FileAPI/templates/empty.odt Deleted
iRony-0.4.3.tar.gz/lib/FileAPI/viewers/doc Deleted
2
 
1
-(directory)
2
iRony-0.4.3.tar.gz/lib/FileAPI/viewers/doc.php Deleted
132
 
1
@@ -1,130 +0,0 @@
2
-<?php
3
-/*
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2011-2016, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-/**
27
- * Class integrating Collabora Online documents viewer
28
- */
29
-class file_viewer_doc extends file_viewer
30
-{
31
-    /**
32
-     * Class constructor
33
-     *
34
-     * @param file_api File API object
35
-     */
36
-    public function __construct($api)
37
-    {
38
-        $this->api = $api;
39
-    }
40
-
41
-    /**
42
-     * Returns list of supported mimetype
43
-     *
44
-     * @return array List of mimetypes
45
-     */
46
-    public function supported_mimetypes()
47
-    {
48
-        $rcube = rcube::get_instance();
49
-
50
-        // Get list of supported types from Collabora
51
-        if ($rcube->config->get('fileapi_wopi_office')) {
52
-            $wopi = new file_wopi($this->api);
53
-            if ($types = $wopi->supported_filetypes()) {
54
-                return $types;
55
-            }
56
-        }
57
-
58
-        return array();
59
-    }
60
-
61
-    /**
62
-     * Check if mimetype is supported by the viewer
63
-     *
64
-     * @param string $mimetype File type
65
-     *
66
-     * @return bool True if mimetype is supported, False otherwise
67
-     */
68
-    public function supports($mimetype)
69
-    {
70
-        return in_array($mimetype, $this->supported_mimetypes());
71
-    }
72
-
73
-    /**
74
-     * Return file viewer URL
75
-     *
76
-     * @param string $file     File name
77
-     * @param string $mimetype File type
78
-     */
79
-    public function href($file, $mimetype = null)
80
-    {
81
-        return $this->api->file_url($file) . '&viewer=doc';
82
-    }
83
-
84
-    /**
85
-     * Print output and exit
86
-     *
87
-     * @param string $file      File name
88
-     * @param array  $file_info File metadata (e.g. type)
89
-     */
90
-    public function output($file, $file_info = array())
91
-    {
92
-        // Create readonly session and get WOPI request parameters
93
-        $wopi = new file_wopi($this->api);
94
-        $url  = $wopi->session_start($file, $file_info, $session, true);
95
-
96
-        if (!$url) {
97
-            $this->api->output_error("Failed to open file", 404);
98
-        }
99
-
100
-        $info = array('readonly' => true);
101
-        $post = $wopi->editor_post_params($info);
102
-        $url  = htmlentities($url);
103
-        $form = '';
104
-
105
-        foreach ($post as $name => $value) {
106
-            $form .= '<input type="hidden" name="' . $name . '" value="' . $value . '" />';
107
-        }
108
-
109
-        echo <<<EOT
110
-<html>
111
-  <head>
112
-    <script src="viewers/doc/file_editor.js" type="text/javascript" charset="utf-8"></script>
113
-    <style>
114
-      iframe, body { width: 100%; height: 100%; margin: 0; border: none; }
115
-      form { display: none; }
116
-    </style>
117
-  </head>
118
-  <body>
119
-    <iframe id="viewer" name="viewer" allowfullscreen></iframe>
120
-    <form target="viewer" method="post" action="$url">
121
-      $form
122
-    </form>
123
-    <script type="text/javascript">
124
-      var file_editor = new file_editor;
125
-      file_editor.init();
126
-    </script>
127
-  </body>
128
-</html>
129
-EOT;
130
-    }
131
-}
132
iRony-0.4.3.tar.gz/lib/FileAPI/viewers/doc/file_editor.js Deleted
36
 
1
@@ -1,34 +0,0 @@
2
-
3
-function file_editor()
4
-{
5
-  this.editable = false;
6
-  this.printable = false;
7
-
8
-  this.init = function()
9
-  {
10
-    document.getElementsByTagName('form')[0].submit();
11
-  };
12
-
13
-  // switch editor into read-write mode
14
-  this.enable = function()
15
-  {
16
-    // @TODO
17
-  };
18
-
19
-  // switch editor into read-only mode
20
-  this.disable = function()
21
-  {
22
-    // @TODO
23
-  };
24
-
25
-  this.getContent = function()
26
-  {
27
-    // @TODO
28
-  };
29
-
30
-  // print file content
31
-  this.print = function()
32
-  {
33
-    // @TODO
34
-  };
35
-}
36
iRony-0.4.3.tar.gz/lib/FileAPI/wopi Deleted
2
 
1
-(directory)
2
iRony-0.4.3.tar.gz/lib/FileAPI/wopi/containers.php Deleted
36
 
1
@@ -1,34 +0,0 @@
2
-<?php
3
-/**
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2018, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-class file_wopi_containers
27
-{
28
-    /**
29
-     * Request handler
30
-     */
31
-    public function handle()
32
-    {
33
-        throw new Exception("Not implemented", file_api_core::ERROR_NOT_IMPLEMENTED);
34
-    }
35
-}
36
iRony-0.4.3.tar.gz/lib/FileAPI/wopi/ecosystem.php Deleted
36
 
1
@@ -1,34 +0,0 @@
2
-<?php
3
-/**
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2018, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-class file_wopi_ecosystem
27
-{
28
-    /**
29
-     * Request handler
30
-     */
31
-    public function handle()
32
-    {
33
-        throw new Exception("Not implemented", file_api_core::ERROR_NOT_IMPLEMENTED);
34
-    }
35
-}
36
iRony-0.4.3.tar.gz/lib/FileAPI/wopi/files.php Deleted
201
 
1
@@ -1,199 +0,0 @@
2
-<?php
3
-/**
4
- +--------------------------------------------------------------------------+
5
- | This file is part of the Kolab File API                                  |
6
- |                                                                          |
7
- | Copyright (C) 2012-2018, Kolab Systems AG                                |
8
- |                                                                          |
9
- | This program is free software: you can redistribute it and/or modify     |
10
- | it under the terms of the GNU Affero General Public License as published |
11
- | by the Free Software Foundation, either version 3 of the License, or     |
12
- | (at your option) any later version.                                      |
13
- |                                                                          |
14
- | This program is distributed in the hope that it will be useful,          |
15
- | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
17
- | GNU Affero General Public License for more details.                      |
18
- |                                                                          |
19
- | You should have received a copy of the GNU Affero General Public License |
20
- | along with this program. If not, see <http://www.gnu.org/licenses/>      |
21
- +--------------------------------------------------------------------------+
22
- | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
23
- +--------------------------------------------------------------------------+
24
-*/
25
-
26
-require_once __DIR__ . "/../api/common.php";
27
-require_once __DIR__ . "/../api/document.php";
28
-
29
-class file_wopi_files extends file_api_document
30
-{
31
-    /**
32
-     * Request handler
33
-     */
34
-    public function handle()
35
-    {
36
-        if (empty($this->api->path)) {
37
-            throw new Exception("File ID not specified", file_api_core::ERROR_NOT_FOUND);
38
-        }
39
-
40
-        $file_id = $this->api->path[0];
41
-        $command = $this->api->path[1];
42
-
43
-        if ($file_id != $_SESSION['document_session']) {
44
-            throw new Exception("File ID not specified", file_api_core::ERROR_UNAUTHORIZED);
45
-        }
46
-
47
-        if ($this->api->method == 'GET') {
48
-            if (empty($command)) {
49
-                return $this->document_info($file_id);
50
-            }
51
-
52
-            if ($command == 'contents') {
53
-                return $this->document_get($file_id);
54
-            }
55
-        }
56
-        else if ($this->api->method == 'PUT') {
57
-            if ($command == 'contents') {
58
-                return $this->document_put($file_id);
59
-            }
60
-        }
61
-/*
62
-        else if (empty($command)) {
63
-            switch ($api->method)
64
-            // TODO case 'UNLOCK':
65
-            // TODO case 'LOCK':
66
-            // TODO case 'GET_LOCK':
67
-            // TODO case 'REFRESH_LOCK':
68
-            // TODO case 'PUT_RELATIVE':
69
-            // TODO case 'RENAME_FILE':
70
-            // TODO case 'DELETE':
71
-            // TODO case 'PUT_USER_INFO':
72
-            // TODO case 'GET_SHARE_URL':
73
-            }
74
-        }
75
-*/
76
-        throw new Exception("Unknown method", file_api_core::ERROR_NOT_IMPLEMENTED);
77
-    }
78
-
79
-    /**
80
-     * Return document informations
81
-     *
82
-     * JSON Response (only required attributes listed):
83
-     * - BaseFileName: The string name of the file without a path. Used for
84
-     *   display in user interface (UI), and determining the extension
85
-     *   of the file.
86
-     * - OwnerId: A string that uniquely identifies the owner of the file.
87
-     * - Size: The size of the file in bytes, expressed as a long,
88
-     *   a 64-bit signed integer.
89
-     * - UserId: A string value uniquely identifying the user currently
90
-     *   accessing the file.
91
-     * - Version: The current version of the file based on the server’s file
92
-     *   version schema, as a string. This value must change when the file changes.
93
-     */
94
-    protected function document_info($id)
95
-    {
96
-        $info = parent::document_info($id);
97
-
98
-        // Convert file metadata to Wopi format
99
-        // TODO: support more properties from
100
-        //       https://wopirest.readthedocs.io/en/latest/files/CheckFileInfo.html
101
-
102
-        $result = array(
103
-            'BaseFileName'              => $info['name'],
104
-            'Size'                      => $info['size'],
105
-            'Version'                   => $info['modified'],
106
-            'OwnerId'                   => $info['owner'],
107
-            'UserId'                    => $info['user'],
108
-            'UserFriendlyName'          => $info['user_name'] ?: preg_replace('/@.*$/', '', $info['user']),
109
-            'UserCanWrite'              => empty($info['readonly']),
110
-            'PostMessageOrigin'         => $info['origin'],
111
-            // Tell the client we do not support PutRelativeFile yet
112
-            'UserCanNotWriteRelative'   => true,
113
-            // Properties specific to Collabora Online
114
-            'HideSaveOption'            => true,
115
-            'HideExportOption'          => true,
116
-            'HidePrintOption'           => true,
117
-            'EnableOwnerTermination'    => true,
118
-            'WatermarkText'             => '', // ??
119
-            'DisablePrint'              => false,
120
-            'DisableExport'             => false,
121
-            'DisableCopy'               => false,
122
-            'DisableInactiveMessages'   => true,
123
-            'DisableChangeTrackingRecord' => true,
124
-            'DisableChangeTrackingShow'   => true,
125
-            'HideChangeTrackingControls'  => true,
126
-            // TODO: 'UserExtraInfo' => ['avatar' => 'http://url/to/user/avatar', 'mail' => $info['user']]
127
-            'UserExtraInfo'             => array(),
128
-        );
129
-
130
-        if ($info['modified']) {
131
-            try {
132
-                $dt = new DateTime('@' . $info['modified'], new DateTimeZone('UTC'));
133
-                $result['LastModifiedTime'] = $dt->format('Y-m-d\TH:i:s') . '.0000000Z';
134
-            }
135
-            catch (Exception $e) {
136
-            }
137
-        }
138
-
139
-        return $result;
140
-    }
141
-
142
-    /**
143
-     * Update document file content
144
-     *
145
-     * Request Headers:
146
-     * - X-WOPI-Lock: A string provided by the WOPI client in a previous Lock request.
147
-     *   Note that this header will not be included during document creation.
148
-     * Collabora-specific Request Headers:
149
-     * - X-LOOL-WOPI-IsModifiedByUser: true/false indicates whether the document
150
-     *   was modified by the user when they saved it.
151
-     * - X-LOOL-WOPI-IsAutosave: true/false indicates whether the PutFile
152
-     *   is a result of autosave or the user pressing the Save button.
153
-     * Response Headers:
154
-     * - X-WOPI-Lock: A string value identifying the current lock on the file.
155
-     *   This header must always be included when responding to the request with 409.
156
-     *   It should not be included when responding to the request with 200 OK.
157
-     * - X-WOPI-LockFailureReason: An optional string value indicating the cause
158
-     *   of a lock failure.
159
-     * - X-WOPI-ItemVersion: An optional string value indicating the version of the file.
160
-     *   Its value should be the same as Version value in CheckFileInfo.
161
-     * Status Codes:
162
-     * - 409 Conflict: Lock mismatch/locked by another interface
163
-     * - 413 Request Entity Too Large: File is too large; Host limit exceeded.
164
-     */
165
-    protected function document_put($file_id)
166
-    {
167
-        // TODO: Locking
168
-
169
-        parent::document_put($file_id);
170
-    }
171
-
172
-    /**
173
-     * Return document file content
174
-     *
175
-     * Request Headers:
176
-     * - X-WOPI-MaxExpectedSize: An integer specifying the upper bound
177
-     *   of the expected size of the file being requested. Optional.
178
-     *   The host should use the maximum value of a 4-byte integer
179
-     *   if this value is not set in the request.
180
-     * Response Headers:
181
-     * - X-WOPI-ItemVersion: An optional string value indicating the version of the file.
182
-     *   Its value should be the same as Version value in CheckFileInfo.
183
-     * Status Codes:
184
-     * - 412 File is larger than X-WOPI-MaxExpectedSize
185
-     */
186
-    protected function document_get($id)
187
-    {
188
-        $doc_info = parent::document_info($id, false);
189
-        $max_size = rcube_utils::request_header('X-WOPI-MaxExpectedSize') ?: 1024 * 1024 * 1024;
190
-
191
-        // Check max file size
192
-        if ($doc_info['size'] > $max_size) {
193
-            throw new Exception("File exceeds max expected size", file_api_core::ERROR_PRECONDITION_FAILED);
194
-        }
195
-
196
-        header("X-WOPI-ItemVersion: " . $doc_info['modified']);
197
-
198
-        parent::document_get($id);
199
-    }
200
-}
201
iRony-0.4.3.tar.gz/composer.lock -> iRony-0.4.4.tar.gz/composer.lock Changed
153
 
1
@@ -252,20 +252,20 @@
2
     "packages-dev": [
3
         {
4
             "name": "doctrine/instantiator",
5
-            "version": "1.3.0",
6
+            "version": "1.3.1",
7
             "source": {
8
                 "type": "git",
9
                 "url": "https://github.com/doctrine/instantiator.git",
10
-                "reference": "ae466f726242e637cebdd526a7d991b9433bacf1"
11
+                "reference": "f350df0268e904597e3bd9c4685c53e0e333feea"
12
             },
13
             "dist": {
14
                 "type": "zip",
15
-                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1",
16
-                "reference": "ae466f726242e637cebdd526a7d991b9433bacf1",
17
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea",
18
+                "reference": "f350df0268e904597e3bd9c4685c53e0e333feea",
19
                 "shasum": ""
20
             },
21
             "require": {
22
-                "php": "^7.1"
23
+                "php": "^7.1 || ^8.0"
24
             },
25
             "require-dev": {
26
                 "doctrine/coding-standard": "^6.0",
27
@@ -304,7 +304,21 @@
28
                 "constructor",
29
                 "instantiate"
30
             ],
31
-            "time": "2019-10-21T16:45:58+00:00"
32
+            "funding": [
33
+                {
34
+                    "url": "https://www.doctrine-project.org/sponsorship.html",
35
+                    "type": "custom"
36
+                },
37
+                {
38
+                    "url": "https://www.patreon.com/phpdoctrine",
39
+                    "type": "patreon"
40
+                },
41
+                {
42
+                    "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
43
+                    "type": "tidelift"
44
+                }
45
+            ],
46
+            "time": "2020-05-29T17:27:14+00:00"
47
         },
48
         {
49
             "name": "kolab/net_ldap3",
50
@@ -455,16 +469,16 @@
51
         },
52
         {
53
             "name": "pear/mail_mime",
54
-            "version": "1.10.4",
55
+            "version": "1.10.9",
56
             "source": {
57
                 "type": "git",
58
                 "url": "https://github.com/pear/Mail_Mime.git",
59
-                "reference": "36ab96ff17c098f65f58adb31c089beb12868ece"
60
+                "reference": "1e7ae4e5258b6c0d385a8e76add567934245d38d"
61
             },
62
             "dist": {
63
                 "type": "zip",
64
-                "url": "https://api.github.com/repos/pear/Mail_Mime/zipball/36ab96ff17c098f65f58adb31c089beb12868ece",
65
-                "reference": "36ab96ff17c098f65f58adb31c089beb12868ece",
66
+                "url": "https://api.github.com/repos/pear/Mail_Mime/zipball/1e7ae4e5258b6c0d385a8e76add567934245d38d",
67
+                "reference": "1e7ae4e5258b6c0d385a8e76add567934245d38d",
68
                 "shasum": ""
69
             },
70
             "require": {
71
@@ -497,7 +511,7 @@
72
             ],
73
             "description": "Mail_Mime provides classes to create MIME messages",
74
             "homepage": "http://pear.php.net/package/Mail_Mime",
75
-            "time": "2019-10-13T09:02:10+00:00"
76
+            "time": "2020-06-27T08:35:27+00:00"
77
         },
78
         {
79
             "name": "pear/net_idna2",
80
@@ -1042,6 +1056,7 @@
81
             "keywords": [
82
                 "tokenizer"
83
             ],
84
+            "abandoned": true,
85
             "time": "2017-12-04T08:55:13+00:00"
86
         },
87
         {
88
@@ -1547,16 +1562,16 @@
89
         },
90
         {
91
             "name": "symfony/polyfill-ctype",
92
-            "version": "v1.13.1",
93
+            "version": "v1.18.1",
94
             "source": {
95
                 "type": "git",
96
                 "url": "https://github.com/symfony/polyfill-ctype.git",
97
-                "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3"
98
+                "reference": "1c302646f6efc070cd46856e600e5e0684d6b454"
99
             },
100
             "dist": {
101
                 "type": "zip",
102
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
103
-                "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
104
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454",
105
+                "reference": "1c302646f6efc070cd46856e600e5e0684d6b454",
106
                 "shasum": ""
107
             },
108
             "require": {
109
@@ -1568,7 +1583,11 @@
110
             "type": "library",
111
             "extra": {
112
                 "branch-alias": {
113
-                    "dev-master": "1.13-dev"
114
+                    "dev-master": "1.18-dev"
115
+                },
116
+                "thanks": {
117
+                    "name": "symfony/polyfill",
118
+                    "url": "https://github.com/symfony/polyfill"
119
                 }
120
             },
121
             "autoload": {
122
@@ -1601,7 +1620,21 @@
123
                 "polyfill",
124
                 "portable"
125
             ],
126
-            "time": "2019-11-27T13:56:44+00:00"
127
+            "funding": [
128
+                {
129
+                    "url": "https://symfony.com/sponsor",
130
+                    "type": "custom"
131
+                },
132
+                {
133
+                    "url": "https://github.com/fabpot",
134
+                    "type": "github"
135
+                },
136
+                {
137
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
138
+                    "type": "tidelift"
139
+                }
140
+            ],
141
+            "time": "2020-07-14T12:35:20+00:00"
142
         },
143
         {
144
             "name": "symfony/yaml",
145
@@ -1664,5 +1697,6 @@
146
     "platform": {
147
         "php": ">=5.4.1"
148
     },
149
-    "platform-dev": []
150
+    "platform-dev": [],
151
+    "plugin-api-version": "1.1.0"
152
 }
153
iRony-0.4.3.tar.gz/config/dav.inc.php.sample -> iRony-0.4.4.tar.gz/config/dav.inc.php.sample Changed
47
 
1
@@ -113,6 +113,45 @@
2
         'changed'           => 'modifytimestamp',
3
     ),
4
 );
5
+
6
+// Expose all resources as an LDAP-based address book in the pricipals address book collection.
7
+// This enables Non-Kolab-Clients to add resources to an event.
8
+// Properties of this option are the same as for $config['kolabdav_ldap_directory'] entries.
9
+$config['kolabdav_ldap_resources']  = array(
10
+    'name'           => 'Global Resources',
11
+    'hosts'          => 'localhost',
12
+    'port'           => 389,
13
+    'use_tls'        => false,
14
+    'user_specific'  => false,
15
+    'search_base_dn' => 'ou=People,dc=example,dc=org',
16
+    'search_bind_dn' => 'uid=kolab-service,ou=Special Users,dc=example,dc=org',
17
+    'search_bind_pw' => 'Welcome2KolabSystems',
18
+    'search_filter'  => '(&(objectClass=inetOrgPerson)(mail=%fu))',
19
+    'base_dn'        => 'ou=Resources,dc=example,dc=org',
20
+    'bind_dn'        => 'uid=kolab-service,ou=Special Users,dc=example,dc=org',
21
+    'bind_pass'      => 'Welcome2KolabSystems',
22
+    'ldap_version'   => 3,
23
+    'filter'         => '(|(objectclass=groupofuniquenames)(objectclass=groupofurls)(objectclass=kolabsharedfolder))',
24
+    'search_fields'  => array('displayname', 'mail'),
25
+    'sort'           => array('displayname', 'sn', 'givenname', 'cn'),
26
+    'scope'          => 'sub',
27
+    'searchonly'     => false,  // Set to false to enable listing
28
+    'sizelimit'      => '1000',
29
+    'timelimit'      => '0',
30
+    'fieldmap'       => array(
31
+        // Internal         => LDAP
32
+        'name'              => 'cn',
33
+        'email'             => 'mail',
34
+        'owner'             => 'owner',
35
+        'description'       => 'description',
36
+        'attributes'        => 'kolabdescattribute',
37
+        'members'           => 'uniquemember',
38
+        // these mappings are required for owner display
39
+        'phone'             => 'telephoneNumber',
40
+        'mobile'            => 'mobile',
41
+    ),
42
+);
43
+
44
 */
45
 
46
 // Enable caching for LDAP directory data.
47
iRony-0.4.3.tar.gz/lib/FileAPI/api/common.php -> iRony-0.4.4.tar.gz/lib/FileAPI/api/common.php Changed
203
 
1
@@ -29,11 +29,10 @@
2
     protected $args = array();
3
 
4
 
5
-    public function __construct($api, $args = array())
6
+    public function __construct($api)
7
     {
8
-        $this->rc   = rcube::get_instance();
9
-        $this->api  = $api;
10
-        $this->args = (array) $args;
11
+        $this->rc  = rcube::get_instance();
12
+        $this->api = $api;
13
     }
14
 
15
     /**
16
@@ -42,11 +41,7 @@
17
     public function handle()
18
     {
19
         // GET arguments
20
-        if (!empty($_GET)) {
21
-            foreach (array_keys($_GET) as $key) {
22
-                $this->args[$key] = &$_GET[$key];
23
-            }
24
-        }
25
+        $this->args = &$_GET;
26
 
27
         // POST arguments (JSON)
28
         if ($_SERVER['REQUEST_METHOD'] == 'POST') {
29
@@ -56,7 +51,7 @@
30
         }
31
 
32
         // disable script execution time limit, so we can handle big files
33
-        @set_time_limit(360);
34
+        @set_time_limit(0);
35
     }
36
 
37
     /**
38
@@ -107,30 +102,22 @@
39
      */
40
     protected function find_viewer($mimetype)
41
     {
42
-        $dir   = RCUBE_INSTALL_PATH . 'lib/viewers';
43
-        $files = array();
44
+        $dir = RCUBE_INSTALL_PATH . 'lib/viewers';
45
 
46
-        // First get viewers and sort by name to get priority
47
         if ($handle = opendir($dir)) {
48
             while (false !== ($file = readdir($handle))) {
49
                 if (preg_match('/^([a-z0-9_]+)\.php$/i', $file, $matches)) {
50
-                    $files[$matches[1]] = $dir . '/' . $file;
51
+                    include_once $dir . '/' . $file;
52
+                    $class  = 'file_viewer_' . $matches[1];
53
+                    $viewer = new $class($this->api);
54
+
55
+                    if ($viewer->supports($mimetype)) {
56
+                        return $viewer;
57
+                    }
58
                 }
59
             }
60
             closedir($handle);
61
         }
62
-
63
-        ksort($files);
64
-
65
-        foreach ($files as $name => $file) {
66
-            include_once $file;
67
-            $class  = 'file_viewer_' . $name;
68
-            $viewer = new $class($this->api);
69
-
70
-            if ($viewer->supports($mimetype)) {
71
-                return $viewer;
72
-            }
73
-        }
74
     }
75
 
76
     /**
77
@@ -153,125 +140,4 @@
78
 
79
         return $metadata;
80
     }
81
-
82
-    /**
83
-     * Get folder rights
84
-     */
85
-    protected function folder_rights($folder)
86
-    {
87
-        list($driver, $path) = $this->api->get_driver($folder);
88
-
89
-        $rights = $driver->folder_rights($path);
90
-        $result = array();
91
-        $map    = array(
92
-            file_storage::ACL_READ  => 'read',
93
-            file_storage::ACL_WRITE => 'write',
94
-        );
95
-
96
-        foreach ($map as $key => $value) {
97
-            if ($rights & $key) {
98
-                $result[] = $value;
99
-            }
100
-        }
101
-
102
-        return $result;
103
-    }
104
-
105
-    /**
106
-     * Collect folder list request parameters
107
-     */
108
-    protected function folder_list_params()
109
-    {
110
-        $params = array('type' => 0);
111
-
112
-        if (!empty($this->args['unsubscribed']) && rcube_utils::get_boolean((string) $this->args['unsubscribed'])) {
113
-            $params['type'] |= file_storage::FILTER_UNSUBSCRIBED;
114
-        }
115
-
116
-        if (!empty($this->args['writable']) && rcube_utils::get_boolean((string) $this->args['writable'])) {
117
-            $params['type'] |= file_storage::FILTER_WRITABLE;
118
-        }
119
-
120
-        if (isset($this->args['search']) && strlen($this->args['search'])) {
121
-            $params['search'] = $this->args['search'];
122
-        }
123
-
124
-        if (!empty($this->args['permissions']) && rcube_utils::get_boolean((string) $this->args['permissions'])) {
125
-            $params['extended']    = true;
126
-            $params['permissions'] = true;
127
-        }
128
-
129
-        if (!empty($this->args['level']) && ($level = intval($this->args['level']))) {
130
-            if ($level < 0) {
131
-                $level *= -1;
132
-                $params['auto_level'] = true;
133
-            }
134
-
135
-            $params['level'] = $level;
136
-        }
137
-
138
-        return $params;
139
-    }
140
-
141
-    /**
142
-     * Wrapper for folder_list() method on specified driver
143
-     */
144
-    protected function folder_list($driver, $params, $relative_level = false)
145
-    {
146
-        $caps = $driver->capabilities();
147
-
148
-        if ($params['type'] & file_storage::FILTER_UNSUBSCRIBED) {
149
-            if (empty($caps[file_storage::CAPS_SUBSCRIPTIONS])) {
150
-                return array();
151
-            }
152
-        }
153
-
154
-        // If the driver has fast way to get the whole folders hierarchy
155
-        // we'll return all folders, despite the requested level, when requested
156
-        if (!empty($params['auto_level']) & !empty($caps[file_storage::CAPS_FAST_FOLDER_LIST])) {
157
-            unset($params['level']);
158
-        }
159
-
160
-        $prefix = $driver->title() . file_storage::SEPARATOR;
161
-
162
-        if (strlen($prefix) > 1 && !$relative_level && $params['level'] && (!is_string($params['path']) || $params['path'] === '')) {
163
-            $params['level'] -= 1;
164
-        }
165
-
166
-        $folders = $driver->folder_list($params);
167
-
168
-        if (!empty($folders) && strlen($prefix) > 1) {
169
-            foreach ($folders as $idx => $folder) {
170
-                if (is_array($folder)) {
171
-                    $folders[$idx]['folder'] = $prefix . $folder['folder'];
172
-                }
173
-                else {
174
-                    $folders[$idx] = $prefix . $folder;
175
-                }
176
-            }
177
-        }
178
-
179
-        return $folders;
180
-    }
181
-
182
-    /**
183
-     * Update document session on file/folder move
184
-     */
185
-    protected function session_uri_update($from, $to, $is_folder = false)
186
-    {
187
-        // check Manticore/WOPI support. Note: we don't use config->get('fileapi_manticore')
188
-        // here as it may be not properly set if backend driver wasn't initialized yet
189
-        $capabilities = $this->api->capabilities(false);
190
-
191
-        if (!empty($capabilities['WOPI'])) {
192
-            $document = new file_wopi($this->api);
193
-        }
194
-        else if (!empty($capabilities['MANTICORE'])) {
195
-            $document = new file_manticore($this->api);
196
-        }
197
-
198
-        if (!empty($document)) {
199
-            $document->session_uri_update($from, $to, $is_folder);
200
-        }
201
-    }
202
 }
203
iRony-0.4.3.tar.gz/lib/FileAPI/api/file_create.php -> iRony-0.4.4.tar.gz/lib/FileAPI/api/file_create.php Changed
61
 
1
@@ -52,30 +52,14 @@
2
             $chunk = $this->args['content'];
3
         }
4
 
5
-        $ctype = $this->args['content-type'];
6
-        if ($ctype && !preg_match('/^[a-z_-]+\/[a-z._-]+$/', $ctype)) {
7
-            $ctype = '';
8
-        }
9
-
10
         $request = $this instanceof file_api_file_update ? 'file_update' : 'file_create';
11
         $file    = array(
12
             'content' => $this->args['content'],
13
             'path'    => $this->args['path'],
14
-            'type'    => rcube_mime::file_content_type($chunk, $this->args['file'], $ctype, !$is_file),
15
+            'type'    => rcube_mime::file_content_type($chunk,
16
+                $this->args['file'], $this->args['content-type'], !$is_file),
17
         );
18
 
19
-        if (strpos($file['type'], 'empty') !== false && $ctype) {
20
-            $file['type'] = $ctype;
21
-        }
22
-        else if (empty($file['type'])) {
23
-            $file['type'] = 'application/octet-stream';
24
-        }
25
-
26
-        // Get file content from a template
27
-        if ($request == 'file_create' && empty($file['path']) && empty($file['content'])) {
28
-            $this->use_file_template($file);
29
-        }
30
-
31
         list($driver, $path) = $this->api->get_driver($this->args['file']);
32
 
33
         $driver->$request($path, $file);
34
@@ -84,26 +68,4 @@
35
             return $driver->file_info($path);
36
         }
37
     }
38
-
39
-    /**
40
-     * Use templates when creating empty files
41
-     */
42
-    protected function use_file_template(&$file)
43
-    {
44
-        if ($ext = array_search($file['type'], file_utils::$ext_map)) {
45
-            // find the template
46
-            $ext = ".$ext";
47
-            if ($handle = opendir(__DIR__ . '/../templates')) {
48
-                while (false !== ($entry = readdir($handle))) {
49
-                    if (substr($entry, -strlen($ext)) == $ext) {
50
-                        // set path to the template file
51
-                        $file['path'] = __DIR__ . '/../templates/' . $entry;
52
-                        break;
53
-                    }
54
-                }
55
-
56
-                closedir($handle);
57
-            }
58
-        }
59
-    }
60
 }
61
iRony-0.4.3.tar.gz/lib/FileAPI/api/file_get.php -> iRony-0.4.4.tar.gz/lib/FileAPI/api/file_get.php Changed
35
 
1
@@ -24,8 +24,6 @@
2
 
3
 class file_api_file_get extends file_api_common
4
 {
5
-    protected $driver;
6
-
7
     /**
8
      * Request handler
9
      */
10
@@ -39,15 +37,9 @@
11
             header("HTTP/1.0 ".file_api_core::ERROR_CODE." Missing file name");
12
         }
13
 
14
-        $method = $_SERVER['REQUEST_METHOD'];
15
-        if ($method == 'POST' && !empty($_SERVER['HTTP_X_HTTP_METHOD'])) {
16
-            $method = $_SERVER['HTTP_X_HTTP_METHOD'];
17
-        }
18
-
19
         $params = array(
20
             'force-download' => rcube_utils::get_boolean((string) $this->args['force-download']),
21
             'force-type'     => $this->args['force-type'],
22
-            'head'           => $this->args['head'] ?: $method == 'HEAD',
23
         );
24
 
25
         list($this->driver, $path) = $this->api->get_driver($this->args['file']);
26
@@ -97,7 +89,7 @@
27
             return;
28
         }
29
 
30
-        $viewer->output($args['file'], $info);
31
+        $viewer->output($args['file'], $info['type']);
32
         exit;
33
     }
34
 }
35
iRony-0.4.3.tar.gz/lib/FileAPI/api/file_info.php -> iRony-0.4.4.tar.gz/lib/FileAPI/api/file_info.php Changed
164
 
1
@@ -3,7 +3,7 @@
2
  +--------------------------------------------------------------------------+
3
  | This file is part of the Kolab File API                                  |
4
  |                                                                          |
5
- | Copyright (C) 2012-2015, Kolab Systems AG                                |
6
+ | Copyright (C) 2012-2014, Kolab Systems AG                                |
7
  |                                                                          |
8
  | This program is free software: you can redistribute it and/or modify     |
9
  | it under the terms of the GNU Affero General Public License as published |
10
@@ -31,69 +31,16 @@
11
     {
12
         parent::handle();
13
 
14
-        // check Manticore support. Note: we don't use config->get('fileapi_manticore')
15
-        // here as it may be not properly set if backend driver wasn't initialized yet
16
-        $capabilities = $this->api->capabilities(false);
17
-        $manticore    = $capabilities['MANTICORE'];
18
-        $wopi         = $capabilities['WOPI'];
19
-
20
-        // support file_info by session ID
21
         if (!isset($this->args['file']) || $this->args['file'] === '') {
22
-            if (($manticore || $wopi) && !empty($this->args['session'])) {
23
-                if ($info = $this->file_document_file($this->args['session'])) {
24
-                    $this->args['file'] = $info['file'];
25
-                }
26
-            }
27
-            else {
28
-                throw new Exception("Missing file name", file_api_core::ERROR_CODE);
29
-            }
30
+            throw new Exception("Missing file name", file_api_core::ERROR_CODE);
31
         }
32
 
33
-        if ($this->args['file'] !== null) {
34
-            try {
35
-                list($driver, $path) = $this->api->get_driver($this->args['file']);
36
+        list($driver, $path) = $this->api->get_driver($this->args['file']);
37
 
38
-                $info = $driver->file_info($path);
39
-                $info['file'] = $this->args['file'];
40
-            }
41
-            catch (Exception $e) {
42
-                // Invited user may have no access to the file,
43
-                // ignore errors if session exists
44
-                if (!$this->args['viewer'] || !$this->args['session']) {
45
-                    throw $e;
46
-                }
47
-            }
48
-        }
49
-
50
-        // Possible 'viewer' types are defined in files_api.js:file_type_supported()
51
-        // 1 - Native browser support
52
-        // 2 - Chwala viewer exists
53
-        // 4 - Editor exists (manticore/wopi)
54
+        $info = $driver->file_info($path);
55
 
56
         if (rcube_utils::get_boolean((string) $this->args['viewer'])) {
57
-            if ($this->args['file'] !== null) {
58
-                $this->file_viewer_info($info);
59
-            }
60
-
61
-            if ((intval($this->args['viewer']) & 4)) {
62
-                // @TODO: Chwala client should have a possibility to select
63
-                //        between wopi and manticore?
64
-                if (!$wopi || !$this->file_wopi_handler($info)) {
65
-                    if ($manticore) {
66
-                        $this->file_manticore_handler($info);
67
-                    }
68
-                }
69
-            }
70
-        }
71
-
72
-        // check writable flag
73
-        if ($this->args['file'] !== null) {
74
-            $path = explode(file_storage::SEPARATOR, $path);
75
-            array_pop($path);
76
-            $path = implode(file_storage::SEPARATOR, $path);
77
-            $acl  = $driver->folder_rights($path);
78
-
79
-            $info['writable'] = ($acl & file_storage::ACL_WRITE) != 0;
80
+            $this->file_viewer_info($this->args['file'], $info);
81
         }
82
 
83
         return $info;
84
@@ -102,12 +49,9 @@
85
     /**
86
      * Merge file viewer data into file info
87
      */
88
-    protected function file_viewer_info(&$info)
89
+    protected function file_viewer_info($file, &$info)
90
     {
91
-        $file   = $this->args['file'];
92
-        $viewer = $this->find_viewer($info['type']);
93
-
94
-        if ($viewer) {
95
+        if ($viewer = $this->find_viewer($info['type'])) {
96
             $info['viewer'] = array();
97
             if ($frame = $viewer->frame($file, $info['type'])) {
98
                 $info['viewer']['frame'] = $frame;
99
@@ -117,64 +61,4 @@
100
             }
101
         }
102
     }
103
-
104
-    /**
105
-     * Get file from manticore/wopi session
106
-     */
107
-    protected function file_document_file($session_id)
108
-    {
109
-        $document = file_document::get_handler($this->api, $session_id);
110
-
111
-        return $document->session_file($session_id, true);
112
-    }
113
-
114
-    /**
115
-     * Merge manticore session data into file info
116
-     */
117
-    protected function file_manticore_handler(&$info)
118
-    {
119
-        $manticore = new file_manticore($this->api);
120
-        $file      = $this->args['file'];
121
-        $session   = $this->args['session'];
122
-
123
-        if (in_array_nocase($info['type'], $manticore->supported_filetypes(true))) {
124
-            $info['viewer']['manticore'] = true;
125
-        }
126
-        else {
127
-            return false;
128
-        }
129
-
130
-        if ($uri = $manticore->session_start($file, $info, $session)) {
131
-            $info['viewer']['href'] = $uri;
132
-            $info['viewer']['post'] = $manticore->editor_post_params($info);
133
-            $info['session']        = $manticore->session_info($session, true);
134
-        }
135
-
136
-        return true;
137
-    }
138
-
139
-    /**
140
-     * Merge WOPI session data into file info
141
-     */
142
-    protected function file_wopi_handler(&$info)
143
-    {
144
-        $wopi    = new file_wopi($this->api);
145
-        $file    = $this->args['file'];
146
-        $session = $this->args['session'];
147
-
148
-        if (in_array_nocase($info['type'], $wopi->supported_filetypes(true))) {
149
-            $info['viewer']['wopi'] = true;
150
-        }
151
-        else {
152
-            return false;
153
-        }
154
-
155
-        if ($uri = $wopi->session_start($file, $info, $session)) {
156
-            $info['viewer']['href'] = $uri;
157
-            $info['viewer']['post'] = $wopi->editor_post_params($info);
158
-            $info['session']        = $wopi->session_info($session, true);
159
-        }
160
-
161
-        return true;
162
-    }
163
 }
164
iRony-0.4.3.tar.gz/lib/FileAPI/api/file_list.php -> iRony-0.4.4.tar.gz/lib/FileAPI/api/file_list.php Changed
13
 
1
@@ -52,6 +52,11 @@
2
 
3
         list($driver, $path) = $this->api->get_driver($this->args['folder']);
4
 
5
+        // mount point contains only folders
6
+        if (!strlen($path)) {
7
+            return array();
8
+        }
9
+
10
         // add mount point prefix to file paths
11
         if ($path != $this->args['folder']) {
12
             $params['prefix'] = substr($this->args['folder'], 0, -strlen($path));
13
iRony-0.4.3.tar.gz/lib/FileAPI/api/file_move.php -> iRony-0.4.4.tar.gz/lib/FileAPI/api/file_move.php Changed
13
 
1
@@ -112,11 +112,6 @@
2
                     throw $e;
3
                 }
4
             }
5
-
6
-            // Update manticore sessions
7
-            if ($request == 'file_move') {
8
-                $this->session_uri_update($file, $new_file, false);
9
-            }
10
         }
11
 
12
         if (!empty($errors)) {
13
iRony-0.4.3.tar.gz/lib/FileAPI/api/folder_auth.php -> iRony-0.4.4.tar.gz/lib/FileAPI/api/folder_auth.php Changed
20
 
1
@@ -70,11 +70,14 @@
2
 
3
         $result = array('folder' => $this->args['folder']);
4
 
5
-        // get list of folders if requested
6
+        // get list if folders if requested
7
         if (rcube_utils::get_boolean((string) $this->args['list'])) {
8
-            $params         = $this->folder_list_params();
9
-            $params['path'] = $path;
10
-            $result['list'] = $this->folder_list($driver, $params, true);
11
+            $prefix         = $this->args['folder'] . file_storage::SEPARATOR;
12
+            $result['list'] = array();
13
+
14
+            foreach ($driver->folder_list() as $folder) {
15
+                $result['list'][] = $prefix . $folder;
16
+            }
17
         }
18
 
19
         return $result;
20
iRony-0.4.3.tar.gz/lib/FileAPI/api/folder_create.php -> iRony-0.4.4.tar.gz/lib/FileAPI/api/folder_create.php Changed
20
 
1
@@ -71,7 +71,7 @@
2
 
3
         // load driver
4
         $driver = $this->api->load_driver_object($this->args['driver']);
5
-        $driver->configure($this->api->env, $this->args['folder']);
6
+        $driver->configure($this->api->config, $this->args['folder']);
7
 
8
         // check if authentication works
9
         $data = $driver->driver_validate($this->args);
10
@@ -90,9 +90,5 @@
11
 
12
         // save the mount point info in config
13
         $backend->driver_create($data);
14
-
15
-        return array(
16
-            'capabilities' => $driver->capabilities(),
17
-        );
18
     }
19
 }
20
iRony-0.4.3.tar.gz/lib/FileAPI/api/folder_list.php -> iRony-0.4.4.tar.gz/lib/FileAPI/api/folder_list.php Changed
160
 
1
@@ -31,90 +31,46 @@
2
     {
3
         parent::handle();
4
 
5
-        $params   = $this->folder_list_params();
6
-        $search   = isset($params['search']) ? mb_strtoupper($params['search']) : null;
7
-        $drivers  = $this->api->get_drivers(true, $admin_drivers);
8
-        $errors   = array();
9
-        $has_more = false;
10
-
11
-        if (isset($this->args['folder']) && strlen($this->args['folder'])) {
12
-            list($driver, $path) = $this->api->get_driver($this->args['folder']);
13
-
14
-            $title          = $driver->title();
15
-            $params['path'] = $path;
16
-
17
-            try {
18
-                $folders = $this->folder_list($driver, $params, true);
19
-            }
20
-            catch (Exception $e) {
21
-                $folders = array();
22
-                if ($e->getCode() == file_storage::ERROR_NOAUTH) {
23
-                    if (!in_array($title, $admin_drivers)) {
24
-                        // inform UI about to ask user for credentials
25
-                        $errors[$title] = $this->parse_metadata($driver->driver_metadata());
26
-                    }
27
-                    else {
28
-                        $errors[$title] = array('error' => file_storage::ERROR_NOAUTH);
29
-                    }
30
-                }
31
-            }
32
-
33
-            $drivers = array();
34
-        }
35
-        else {
36
-            $backend = $this->api->get_backend();
37
-
38
-            // get folders from default driver
39
-            if (!$this->rc->config->get('fileapi_backend_storage_disabled')) {
40
-                $folders = $this->folder_list($backend, $params);
41
-            }
42
-        }
43
+        // get folders from main driver
44
+        $backend = $this->api->get_backend();
45
+        $folders = $backend->folder_list();
46
 
47
         // old result format
48
         if ($this->api->client_version() < 2) {
49
             return $folders;
50
         }
51
 
52
+        $drivers  = $this->api->get_drivers(true);
53
+        $has_more = false;
54
+        $errors   = array();
55
+
56
         // get folders from external sources
57
         foreach ($drivers as $driver) {
58
             $title  = $driver->title();
59
             $prefix = $title . file_storage::SEPARATOR;
60
 
61
             // folder exists in main source, replace it with external one
62
-            foreach ($folders as $idx => $folder) {
63
-                if (is_array($folder)) {
64
-                    $folder = $folder['folder'];
65
-                }
66
-                if ($folder == $title || strpos($folder, $prefix) === 0) {
67
-                    unset($folders[$idx]);
68
+            if (($idx = array_search($title, $folders)) !== false) {
69
+                foreach ($folders as $idx => $folder) {
70
+                    if ($folder == $title || strpos($folder, $prefix) === 0) {
71
+                        unset($folders[$idx]);
72
+                    }
73
                 }
74
             }
75
 
76
-            if ($search === null || strpos(mb_strtoupper($title), $search) !== false) {
77
-                if ($folder = $this->driver_root_folder($driver, $params)) {
78
-                    $has_more  = $has_more || count($folders) > 0;
79
-                    $folders[] = $folder;
80
-                }
81
-            }
82
+            $folders[] = $title;
83
+            $has_more  = true;
84
 
85
-            if ($driver != $backend && $params['level'] != 1) {
86
+            if ($driver != $backend) {
87
                 try {
88
-                    $_folders = $this->folder_list($driver, $params);
89
-                    if (!empty($_folders)) {
90
-                        $folders  = array_merge($folders, $_folders);
91
-                        $has_more = true;
92
-                        unset($_folders);
93
+                    foreach ($driver->folder_list() as $folder) {
94
+                        $folders[] = $prefix . $folder;
95
                     }
96
                 }
97
                 catch (Exception $e) {
98
                     if ($e->getCode() == file_storage::ERROR_NOAUTH) {
99
-                        if (!in_array($title, $admin_drivers)) {
100
-                            // inform UI about to ask user for credentials
101
-                            $errors[$title] = $this->parse_metadata($driver->driver_metadata());
102
-                        }
103
-                        else {
104
-                            $errors[$title] = array('error' => file_storage::ERROR_NOAUTH);
105
-                        }
106
+                        // inform UI about to ask user for credentials
107
+                        $errors[$title] = $this->parse_metadata($driver->driver_metadata());
108
                     }
109
                 }
110
             }
111
@@ -122,33 +78,32 @@
112
 
113
         // re-sort the list
114
         if ($has_more) {
115
-            usort($folders, array('file_utils', 'sort_folder_comparator'));
116
+            usort($folders, array($this, 'sort_folder_comparator'));
117
         }
118
 
119
         return array(
120
-            'list'        => array_values($folders),
121
+            'list'        => $folders,
122
             'auth_errors' => $errors,
123
         );
124
     }
125
 
126
-    protected function driver_root_folder($driver, $params)
127
+    /**
128
+     * Callback for uasort() that implements correct
129
+     * locale-aware case-sensitive sorting
130
+     */
131
+    protected function sort_folder_comparator($str1, $str2)
132
     {
133
-        $title  = $driver->title();
134
-        $folder = $params['extended'] ? array('folder' => $title) : $title;
135
+        $path1 = explode(file_storage::SEPARATOR, $str1);
136
+        $path2 = explode(file_storage::SEPARATOR, $str2);
137
 
138
-        if ($params['permissions'] || ($params['type'] & file_storage::FILTER_WRITABLE)) {
139
-            if ($readonly = !($driver->folder_rights('') & file_storage::ACL_WRITE)) {
140
-                if ($params['permissions']) {
141
-                    $folder['readonly'] = true;
142
-                }
143
+        foreach ($path1 as $idx => $folder1) {
144
+            $folder2 = $path2[$idx];
145
+
146
+            if ($folder1 === $folder2) {
147
+                continue;
148
             }
149
-        }
150
-        else {
151
-            $readonly = false;
152
-        }
153
 
154
-        if (!$readonly || !($params['type'] & file_storage::FILTER_WRITABLE)) {
155
-            return $folder;
156
+            return strcoll($folder1, $folder2);
157
         }
158
     }
159
 }
160
iRony-0.4.3.tar.gz/lib/FileAPI/api/folder_move.php -> iRony-0.4.4.tar.gz/lib/FileAPI/api/folder_move.php Changed
18
 
1
@@ -68,14 +68,10 @@
2
                 throw new Exception("Destination folder already exists", file_api_core::ERROR_CODE);
3
             }
4
 
5
-            $this->folder_move_to_other_driver($src_driver, $src_path, $dst_driver, $dst_path);
6
-        }
7
-        else {
8
-            $src_driver->folder_move($src_path, $dst_path);
9
+            return $this->folder_move_to_other_driver($src_driver, $src_path, $dst_driver, $dst_path);
10
         }
11
 
12
-        // Update manticore
13
-        $this->session_uri_update($this->args['folder'], $this->args['new'], true);
14
+        return $src_driver->folder_move($src_path, $dst_path);
15
     }
16
 
17
     /**
18
iRony-0.4.3.tar.gz/lib/FileAPI/api/folder_types.php -> iRony-0.4.4.tar.gz/lib/FileAPI/api/folder_types.php Changed
41
 
1
@@ -32,7 +32,6 @@
2
         parent::handle();
3
 
4
         $drivers = $this->rc->config->get('fileapi_drivers');
5
-        $presets = (array) $this->rc->config->get('fileapi_presets');
6
         $result  = array();
7
 
8
         if (!empty($drivers)) {
9
@@ -40,22 +39,20 @@
10
                 if ($driver_name != 'kolab' && !isset($result[$driver_name])) {
11
                     $driver = $this->api->load_driver_object($driver_name);
12
                     $meta   = $driver->driver_metadata();
13
-                    $meta   = $this->parse_metadata($meta);
14
 
15
-                    if (!empty($presets[$driver_name]) && empty($meta['form_values'])) {
16
-                        $meta['form_values'] = (array) $presets[$driver_name];
17
-                        $user = $this->rc->get_user_name();
18
-
19
-                        foreach ($meta['form_values'] as $key => $val) {
20
-                            $meta['form_values'][$key] = str_replace('%u', $user, $val);
21
-                        }
22
-                    }
23
-
24
-                    $result[$driver_name] = $meta;
25
+                    $result[$driver_name] = $this->parse_metadata($meta);
26
                 }
27
             }
28
         }
29
+/*
30
+        // add local storage to the list
31
+        if (!empty($result)) {
32
+            $backend = $this->api->get_backend();
33
+            $meta = $backend->driver_metadata();
34
 
35
+            $result = array_merge(array('default' => $this->parse_metadata($meta, true)), $result);
36
+        }
37
+*/
38
         return $result;
39
     }
40
 }
41
iRony-0.4.3.tar.gz/lib/FileAPI/drivers/kolab/kolab_file_storage.php -> iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/kolab_file_storage.php Changed
1049
 
1
@@ -44,10 +44,6 @@
2
      */
3
     protected $title;
4
 
5
-    /**
6
-     * @var array
7
-     */
8
-    protected $icache = array();
9
 
10
     /**
11
      * Class constructor
12
@@ -60,8 +56,8 @@
13
         // WARNING: We can use only plugins that are prepared for this
14
         //          e.g. are not using output or rcmail objects or
15
         //          doesn't throw errors when using them
16
-        $plugins = (array) $this->rc->config->get('fileapi_plugins', array('kolab_auth', 'kolab_folders'));
17
-        $plugins = array_unique(array_merge($plugins, array('libkolab')));
18
+        $plugins  = (array)$this->rc->config->get('fileapi_plugins', array('kolab_auth'));
19
+        $required = array('libkolab');
20
 
21
         // Kolab WebDAV server supports plugins, no need to overwrite object
22
         if (!is_a($this->rc->plugins, 'rcube_plugin_api')) {
23
@@ -70,11 +66,7 @@
24
             $this->rc->plugins->init($this, '');
25
         }
26
 
27
-        // this way we're compatible with Roundcube Framework 1.2
28
-        // we can't use load_plugins() here
29
-        foreach ($plugins as $plugin) {
30
-            $this->rc->plugins->load_plugin($plugin, true);
31
-        }
32
+        $this->rc->plugins->load_plugins($plugins, $required);
33
 
34
         $this->init();
35
     }
36
@@ -98,16 +90,10 @@
37
 
38
         // Authenticate - get Roundcube user ID
39
         if ($auth['valid'] && !$auth['abort']
40
-            && ($this->login($auth['user'], $auth['pass'], $auth['host'], $err))) {
41
+            && ($this->login($auth['user'], $auth['pass'], $auth['host']))) {
42
             return true;
43
         }
44
 
45
-        if ($err) {
46
-            $err_str = $this->rc->get_storage()->get_error_str();
47
-        }
48
-
49
-        kolab_auth::log_login_error($auth['user'], $err_str ?: $err);
50
-
51
         $this->rc->plugins->exec_hook('login_failed', array(
52
             'host' => $auth['host'],
53
             'user' => $auth['user'],
54
@@ -165,7 +151,7 @@
55
     /**
56
      * Authenticates a user in IMAP
57
      */
58
-    private function login($username, $password, $host, &$error = null)
59
+    private function login($username, $password, $host)
60
     {
61
         if (empty($username)) {
62
             return false;
63
@@ -217,7 +203,6 @@
64
         // authenticate user in IMAP
65
         $storage = $this->rc->get_storage();
66
         if (!$storage->connect($host, $username, $password, $port, $ssl)) {
67
-            $error = $storage->get_error_code();
68
             return false;
69
         }
70
 
71
@@ -255,16 +240,13 @@
72
         $this->init($user);
73
 
74
         // force reloading of mailboxes list/data
75
-        // Roundcube already does that (T1050)
76
-        //$storage->clear_cache('mailboxes', true);
77
+        $storage->clear_cache('mailboxes', true);
78
 
79
         return true;
80
     }
81
 
82
     protected function init($user = null)
83
     {
84
-        $this->rc->plugins->exec_hook('startup');
85
-
86
         if ($_SESSION['user_id'] || $user) {
87
             // overwrite config with user preferences
88
             $this->rc->user = $user ? $user : new rcube_user($_SESSION['user_id']);
89
@@ -317,12 +299,9 @@
90
         $quota   = $storage->get_capability('QUOTA');
91
 
92
         return array(
93
-            file_storage::CAPS_MAX_UPLOAD    => $max_filesize,
94
-            file_storage::CAPS_QUOTA         => $quota,
95
-            file_storage::CAPS_LOCKS         => true,
96
-            file_storage::CAPS_SUBSCRIPTIONS => true,
97
-            file_storage::CAPS_ACL           => true,
98
-            file_storage::CAPS_FAST_FOLDER_LIST => true,
99
+            file_storage::CAPS_MAX_UPLOAD => $max_filesize,
100
+            file_storage::CAPS_QUOTA      => $quota,
101
+            file_storage::CAPS_LOCKS      => true,
102
         );
103
     }
104
 
105
@@ -481,7 +460,7 @@
106
      */
107
     public function file_create($file_name, $file)
108
     {
109
-        $exists = $this->get_file_object_fast($file_name, $folder);
110
+        $exists = $this->get_file_object($file_name, $folder);
111
         if (!empty($exists)) {
112
             throw new Exception("Storage error. File exists.", file_storage::ERROR);
113
         }
114
@@ -554,7 +533,7 @@
115
      */
116
     public function file_delete($file_name)
117
     {
118
-        $file = $this->get_file_object_fast($file_name, $folder);
119
+        $file = $this->get_file_object($file_name, $folder);
120
         if (empty($file)) {
121
             throw new Exception("Storage error. File not found.", file_storage::ERROR);
122
         }
123
@@ -582,7 +561,7 @@
124
      */
125
     public function file_get($file_name, $params = array(), $fp = null)
126
     {
127
-        $file = $this->get_file_object_fast($file_name, $folder, true);
128
+        $file = $this->get_file_object($file_name, $folder);
129
         if (empty($file)) {
130
             throw new Exception("Storage error. File not found.", file_storage::ERROR);
131
         }
132
@@ -626,7 +605,7 @@
133
         header("Content-Length: " . $file['size']);
134
         header("Content-Disposition: $disposition; filename=\"$filename\"");
135
 
136
-        if ($file['size'] && empty($params['head'])) {
137
+        if ($file['size']) {
138
             $folder->get_attachment($file['_msguid'], $file['fileid'], $file['_mailbox'], true);
139
         }
140
     }
141
@@ -640,7 +619,7 @@
142
      */
143
     public function file_info($file_name)
144
     {
145
-        $file = $this->get_file_object_fast($file_name, $folder, true);
146
+        $file = $this->get_file_object($file_name, $folder);
147
         if (empty($file)) {
148
             throw new Exception("Storage error. File not found.", file_storage::ERROR);
149
         }
150
@@ -651,8 +630,8 @@
151
             'name'     => $file['name'],
152
             'size'     => (int) $file['size'],
153
             'type'     => (string) $file['type'],
154
-            'mtime'    => file_utils::date_format($file['changed'], $this->config['date_format'], $this->config['timezone']),
155
-            'ctime'    => file_utils::date_format($file['created'], $this->config['date_format'], $this->config['timezone']),
156
+            'mtime'    => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
157
+            'ctime'    => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
158
             'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
159
             'created'  => $file['created'] ? $file['created']->format('U') : 0,
160
         );
161
@@ -688,7 +667,8 @@
162
         }
163
 
164
         // get files list
165
-        $files  = $this->get_files($folder_name, $filter);
166
+        $folder = $this->get_folder_object($folder_name);
167
+        $files  = $folder->select($filter);
168
         $result = array();
169
 
170
         // convert to kolab_storage files list data format
171
@@ -705,8 +685,8 @@
172
                 'name'     => $file['name'],
173
                 'size'     => (int) $file['size'],
174
                 'type'     => (string) $file['type'],
175
-                'mtime'    => file_utils::date_format($file['changed'], $this->config['date_format'], $this->config['timezone']),
176
-                'ctime'    => file_utils::date_format($file['created'], $this->config['date_format'], $this->config['timezone']),
177
+                'mtime'    => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
178
+                'ctime'    => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
179
                 'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
180
                 'created'  => $file['created'] ? $file['created']->format('U') : 0,
181
             );
182
@@ -752,7 +732,7 @@
183
             throw new Exception("Storage error. File not found.", file_storage::ERROR);
184
         }
185
 
186
-        $new = $this->get_file_object_fast($new_name, $new_folder);
187
+        $new = $this->get_file_object($new_name, $new_folder);
188
         if (!empty($new)) {
189
             throw new Exception("Storage error. File exists.", file_storage::ERROR_FILE_EXISTS);
190
         }
191
@@ -781,7 +761,7 @@
192
         // Update object
193
         $file['_attachments'] = array(
194
             0 => array(
195
-                'name'     => $new_name,
196
+                'name'     => $file['name'],
197
                 'path'     => $file_path,
198
                 'mimetype' => $file['type'],
199
                 'size'     => $file['size'],
200
@@ -820,7 +800,7 @@
201
             throw new Exception("Storage error. File not found.", file_storage::ERROR);
202
         }
203
 
204
-        $new = $this->get_file_object_fast($new_name, $new_folder);
205
+        $new = $this->get_file_object($new_name, $new_folder);
206
         if (!empty($new)) {
207
             throw new Exception("Storage error. File exists.", file_storage::ERROR_FILE_EXISTS);
208
         }
209
@@ -872,11 +852,11 @@
210
      */
211
     public function folder_create($folder_name)
212
     {
213
-        $folder_name = $this->folder_in($folder_name);
214
-        $success     = kolab_storage::folder_create($folder_name, 'file', true);
215
+        $folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
216
+        $success     = kolab_storage::folder_create($folder_name, 'file');
217
 
218
         if (!$success) {
219
-            throw new Exception("Storage error. Unable to create the folder", file_storage::ERROR);
220
+            throw new Exception("Storage error. Unable to create folder", file_storage::ERROR);
221
         }
222
     }
223
 
224
@@ -889,11 +869,11 @@
225
      */
226
     public function folder_delete($folder_name)
227
     {
228
-        $folder_name = $this->folder_in($folder_name);
229
+        $folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
230
         $success     = kolab_storage::folder_delete($folder_name);
231
 
232
         if (!$success) {
233
-            throw new Exception("Storage error. Unable to delete the folder.", file_storage::ERROR);
234
+            throw new Exception("Storage error. Unable to delete folder.", file_storage::ERROR);
235
         }
236
     }
237
 
238
@@ -907,211 +887,44 @@
239
      */
240
     public function folder_move($folder_name, $new_name)
241
     {
242
-        $folder_name = $this->folder_in($folder_name);
243
-        $new_name    = $this->folder_in($new_name);
244
+        $folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
245
+        $new_name    = rcube_charset::convert($new_name, RCUBE_CHARSET, 'UTF7-IMAP');
246
         $success     = kolab_storage::folder_rename($folder_name, $new_name);
247
 
248
         if (!$success) {
249
-            throw new Exception("Storage error. Unable to rename the folder", file_storage::ERROR);
250
-        }
251
-    }
252
-
253
-    /**
254
-     * Subscribe a folder.
255
-     *
256
-     * @param string $folder_name Name of a folder with full path
257
-     *
258
-     * @throws Exception
259
-     */
260
-    public function folder_subscribe($folder_name)
261
-    {
262
-        $folder_name = $this->folder_in($folder_name);
263
-        $storage     = $this->rc->get_storage();
264
-
265
-        if (!$storage->subscribe($folder_name)) {
266
-            throw new Exception("Storage error. Unable to subscribe the folder", file_storage::ERROR);
267
-        }
268
-    }
269
-
270
-    /**
271
-     * Unsubscribe a folder.
272
-     *
273
-     * @param string $folder_name Name of a folder with full path
274
-     *
275
-     * @throws Exception
276
-     */
277
-    public function folder_unsubscribe($folder_name)
278
-    {
279
-        $folder_name = $this->folder_in($folder_name);
280
-        $storage     = $this->rc->get_storage();
281
-
282
-        if (!$storage->unsubscribe($folder_name)) {
283
-            throw new Exception("Storage error. Unable to unsubsribe the folder", file_storage::ERROR);
284
+            throw new Exception("Storage error. Unable to rename folder", file_storage::ERROR);
285
         }
286
     }
287
 
288
     /**
289
      * Returns list of folders.
290
      *
291
-     * @param array $params List parameters ('type', 'search', 'extended', 'permissions', 'level', 'path')
292
-     *
293
      * @return array List of folders
294
      * @throws Exception
295
      */
296
-    public function folder_list($params = array())
297
+    public function folder_list()
298
     {
299
-        $unsubscribed = $params['type'] & file_storage::FILTER_UNSUBSCRIBED;
300
-        $rights       = ($params['type'] & file_storage::FILTER_WRITABLE) ? 'w' : null;
301
-        $imap         = $this->rc->get_storage();
302
-        $separator    = $imap->get_hierarchy_delimiter();
303
-        $root         = isset($params['path']) && strlen($params['path']) ? $this->folder_in($params['path']) . '/' : '';
304
-        $folders      = $imap->list_folders_subscribed($root, '*', 'file', $rights);
305
+        $folders = kolab_storage::list_folders('', '*', 'file', false);
306
 
307
         if (!is_array($folders)) {
308
             throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR);
309
         }
310
 
311
-        // create/subscribe 'Files' folder in case there's no folder of type 'file'
312
-        if (empty($folders) && !$unsubscribed && !strlen($root)) {
313
-            $default = 'Files';
314
-
315
-            // the folder may exist but be unsubscribed
316
-            if (!$imap->folder_exists($default)) {
317
-                if (kolab_storage::folder_create($default, 'file', true)) {
318
-                    $folders[] = $default;
319
-                }
320
-            }
321
-            else if (kolab_storage::folder_type($default) == 'file') {
322
-                if ($imap->subscribe($default)) {
323
-                    $folders[] = $default;
324
-                }
325
+        // create 'Files' folder in case there's no folder of type 'file'
326
+        if (empty($folders)) {
327
+            if (kolab_storage::folder_create('Files', 'file')) {
328
+                $folders[] = 'Files';
329
             }
330
         }
331
         else {
332
-            if ($unsubscribed) {
333
-                $subscribed = $folders;
334
-                $folders    = $imap->list_folders($root, '*', 'file', $rights);
335
-                $folders    = array_diff($folders, $subscribed);
336
-            }
337
-
338
-            $folders = array_map(array($this, 'folder_out'), $folders);
339
-        }
340
-
341
-        // This could probably be optimized by doing a direct
342
-        // IMAP LIST command with prepared second argument, but
343
-        // it would make caching not optimal
344
-        if ($params['level'] > 0) {
345
-            $offset = isset($params['path']) && strlen($params['path']) ? strlen($params['path']) + 1 : 0;
346
-            foreach ($folders as $idx => $folder) {
347
-                if (substr_count($folder, $separator, $offset) >= $params['level']) {
348
-                    unset($folders[$idx]);
349
-                }
350
-            }
351
-        }
352
-
353
-        // searching
354
-        if (isset($params['search'])) {
355
-            $search  = mb_strtoupper($params['search']);
356
-            $prefix  = null;
357
-            $ns      = $imap->get_namespace('other');
358
-
359
-            if (!empty($ns)) {
360
-                $prefix = rcube_charset::convert($ns[0][0], 'UTF7-IMAP', RCUBE_CHARSET);
361
-            }
362
-
363
-            $folders = array_filter($folders, function($folder) use ($search, $prefix) {
364
-                $path = explode('/', $folder);
365
-
366
-                // search in folder name not the full path
367
-                if (strpos(mb_strtoupper($path[count($path)-1]), $search) !== false) {
368
-                    return true;
369
-                }
370
-                // if it is an other user folder, we'll match the user name
371
-                // and return all folders of the matching user
372
-                else if (strpos($folder, $prefix) === 0 && strpos(mb_strtoupper($path[1]), $search) !== false) {
373
-                    return true;
374
-                }
375
-
376
-                return false;
377
-            });
378
-        }
379
-
380
-        $folders = array_values($folders);
381
-
382
-        // In extended format we return array of arrays
383
-        if ($params['extended']) {
384
-            if (!$rights && $params['permissions']) {
385
-                // get list of known writable folders from cache
386
-                $cache_key   = 'mailboxes.permissions';
387
-                $permissions = (array) $imap->get_cache($cache_key);
388
-            }
389
-
390
-            foreach ($folders as $idx => $folder_name) {
391
-                $folder = array('folder' => $folder_name);
392
-
393
-                // check if folder is readonly
394
-                if (isset($permissions)) {
395
-                    if (!array_key_exists($folder_name, $permissions)) {
396
-                        $acl = $this->folder_rights($folder_name);
397
-                        $permissions[$folder_name] = $acl;
398
-                    }
399
-
400
-                    if (!($permissions[$folder_name] & file_storage::ACL_WRITE)) {
401
-                        $folder['readonly'] = true;
402
-                    }
403
-                }
404
-
405
-                $folders[$idx] = $folder;
406
-            }
407
-
408
-            if ($cache_key) {
409
-                $imap->update_cache($cache_key, $permissions);
410
-            }
411
+            $callback = function($folder) { return rcube_charset::convert($folder, 'UTF7-IMAP', RCUBE_CHARSET); };
412
+            $folders  = array_map($callback, $folders);
413
         }
414
 
415
         return $folders;
416
     }
417
 
418
     /**
419
-     * Check folder rights.
420
-     *
421
-     * @param string $folder Folder name
422
-     *
423
-     * @return int Folder rights (sum of file_storage::ACL_*)
424
-     */
425
-    public function folder_rights($folder)
426
-    {
427
-        $storage = $this->rc->get_storage();
428
-        $folder  = $this->folder_in($folder);
429
-        $rights  = file_storage::ACL_READ;
430
-
431
-        // get list of known writable folders from cache
432
-        $cache_key   = 'mailboxes.permissions';
433
-        $permissions = (array) $storage->get_cache($cache_key);
434
-
435
-        if (array_key_exists($folder, $permissions)) {
436
-            return $permissions[$folder];
437
-        }
438
-
439
-        // For better performance, assume personal folders are writeable
440
-        if ($storage->folder_namespace($folder) == 'personal') {
441
-            $rights |= file_storage::ACL_WRITE;
442
-        }
443
-        else {
444
-            $myrights = $storage->my_rights($folder);
445
-
446
-            if (in_array('t', (array) $myrights)) {
447
-                $rights |= file_storage::ACL_WRITE;
448
-            }
449
-
450
-            $permissions[$folder] = $rights;
451
-            $storage->update_cache($cache_key, $permissions);
452
-        }
453
-
454
-        return $rights;
455
-    }
456
-
457
-    /**
458
      * Returns a list of locks
459
      *
460
      * This method should return all the locks for a particular URI, including
461
@@ -1120,25 +933,25 @@
462
      * If child_locks is set to true, this method should also look for
463
      * any locks in the subtree of the URI for locks.
464
      *
465
-     * @param string $path        File/folder path
466
+     * @param string $uri         URI
467
      * @param bool   $child_locks Enables subtree checks
468
      *
469
      * @return array List of locks
470
      * @throws Exception
471
      */
472
-    public function lock_list($path, $child_locks = false)
473
+    public function lock_list($uri, $child_locks = false)
474
     {
475
         $this->init_lock_db();
476
 
477
         // convert URI to global resource string
478
-        $uri = $this->path2uri($path);
479
+        $uri = $this->uri2resource($uri);
480
 
481
         // get locks list
482
         $list = $this->lock_db->lock_list($uri, $child_locks);
483
 
484
         // convert back resource string into URIs
485
         foreach ($list as $idx => $lock) {
486
-            $list[$idx]['uri'] = $this->uri2path($lock['uri']);
487
+            $list[$idx]['uri'] = $this->resource2uri($lock['uri']);
488
         }
489
 
490
         return $list;
491
@@ -1147,7 +960,7 @@
492
     /**
493
      * Locks a URI
494
      *
495
-     * @param string $path File/folder path
496
+     * @param string $uri  URI
497
      * @param array  $lock Lock data
498
      *                     - depth: 0/'infinite'
499
      *                     - scope: 'shared'/'exclusive'
500
@@ -1157,12 +970,12 @@
501
      *
502
      * @throws Exception
503
      */
504
-    public function lock($path, $lock)
505
+    public function lock($uri, $lock)
506
     {
507
         $this->init_lock_db();
508
 
509
         // convert URI to global resource string
510
-        $uri = $this->path2uri($path);
511
+        $uri = $this->uri2resource($uri);
512
 
513
         if (!$this->lock_db->lock($uri, $lock)) {
514
             throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR);
515
@@ -1172,17 +985,17 @@
516
     /**
517
      * Removes a lock from a URI
518
      *
519
-     * @param string $path File/folder path
520
+     * @param string $path URI
521
      * @param array  $lock Lock data
522
      *
523
      * @throws Exception
524
      */
525
-    public function unlock($path, $lock)
526
+    public function unlock($uri, $lock)
527
     {
528
         $this->init_lock_db();
529
 
530
-        // convert path to global resource string
531
-        $uri = $this->path2uri($path);
532
+        // convert URI to global resource string
533
+        $uri = $this->uri2resource($uri);
534
 
535
         if (!$this->lock_db->unlock($uri, $lock)) {
536
             throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR);
537
@@ -1209,295 +1022,16 @@
538
     }
539
 
540
     /**
541
-     * Sharing interface
542
-     *
543
-     * @param string $folder_name Name of a folder with full path
544
-     * @param int    $mode        Sharing action mode
545
-     * @param array  $args        POST/GET parameters
546
-     *
547
-     * @return mixed Sharing response
548
-     * @throws Exception
549
-     */
550
-    public function sharing($folder, $mode, $args = array())
551
-    {
552
-        $folder_name = $this->folder_in($folder);
553
-        $storage     = $this->rc->get_storage();
554
-        $folder_info = $storage->folder_info($folder_name);
555
-
556
-        if (!is_array($folder_info['rights'])) {
557
-            throw new Exception("Storage error. Failed to get folder permissions.", file_storage::ERROR);
558
-        }
559
-
560
-        if (!in_array('a', $folder_info['rights'])) {
561
-            throw new Exception("No permissions to administer this folder.", file_storage::ERROR_FORBIDDEN);
562
-        }
563
-
564
-        if ($mode == file_storage::SHARING_MODE_FORM) {
565
-            $form = array(
566
-                'shares' => array(
567
-                    'title' => 'share.permissions',
568
-                    'form'  => array(
569
-                        'user' => array(
570
-                            'title' => 'share.usergroup',
571
-                            'type'  => 'input',
572
-                            'autocomplete' => 'user,group',
573
-                        ),
574
-                        'right' => array(
575
-                            'title' => 'share.permission',
576
-                            'type'  => 'select',
577
-                            'options' => array(
578
-                                'r'  => 'share.readonly',
579
-                                'rw' => 'share.readwrite',
580
-                                'a' => 'share.admin',
581
-                            ),
582
-                        ),
583
-                    ),
584
-                    'extra_fields' => array(
585
-                        'type' => 'user',
586
-                        'id'   => '',
587
-                    ),
588
-                ),
589
-            );
590
-
591
-            return $form;
592
-        }
593
-
594
-        if ($mode == file_storage::SHARING_MODE_RIGHTS) {
595
-            $result   = array();
596
-            $acl_list = $storage->get_acl($folder_name);
597
-            $ac       = new kolab_file_autocomplete($this);
598
-
599
-            foreach ((array) $acl_list as $name => $acl) {
600
-                if ($name == $_SESSION['username']) {
601
-                    continue;
602
-                }
603
-
604
-                if (in_array('a', $acl)) {
605
-                    $right = 'a';
606
-                }
607
-                else if (in_array('i', $acl)) {
608
-                    $right = 'rw';
609
-                }
610
-                else if (in_array('r', $acl)) {
611
-                    $right = 'r';
612
-                }
613
-                else {
614
-                    continue;
615
-                }
616
-
617
-                $type    = strpos($name, 'group:') === 0 ? 'group' : 'user';
618
-                $id      = $name;
619
-                $display = $ac->resolve_uid($id, $title);
620
-
621
-                $result[] = array(
622
-                    'mode'  => 'shares',
623
-                    'type'  => $type,
624
-                    'right' => $right,
625
-                    'user'  => $display,
626
-                    'title' => $title,
627
-                    'id'    => $id,
628
-                );
629
-            }
630
-
631
-            return $result;
632
-        }
633
-
634
-        if ($mode == file_storage::SHARING_MODE_UPDATE) {
635
-            if ($args['mode'] == 'shares') {
636
-                $user = $args['id'];
637
-                if (!$user) {
638
-                    $user = ($args['type'] == 'group' ? 'group:' : '') . preg_replace('/^group:/', '', $args['user']);
639
-                }
640
-
641
-                switch ($args['right']) {
642
-                case 'r':  $acl = 'lrs'; break;
643
-                case 'rw': $acl = 'lrswite'; break;
644
-                case 'a':  $acl = 'lrswiteax'; break;
645
-                }
646
-
647
-                if (empty($user) || (empty($acl) && $args['action'] != 'delete')) {
648
-                    throw new Exception("Invalid input.", file_storage::ERROR);
649
-                }
650
-
651
-                switch ($args['action']) {
652
-                case 'submit':
653
-                case 'update':
654
-                    if ($result = $storage->set_acl($folder_name, $user, $acl)) {
655
-                        $ac      = new kolab_file_autocomplete($this);
656
-                        $display = $ac->resolve_uid($user, $title);
657
-                        $result  = array('display' => $display, 'title' => $title);
658
-                    }
659
-                    break;
660
-
661
-                case 'delete':
662
-                    $result = $storage->delete_acl($folder_name, $user);
663
-                    break;
664
-                }
665
-            }
666
-            else {
667
-                throw new Exception("Invalid input.", file_storage::ERROR);
668
-            }
669
-
670
-            if (empty($result)) {
671
-                throw new Exception("Storage error. Failed to update share.", file_storage::ERROR);
672
-            }
673
-
674
-            return $result;
675
-        }
676
-    }
677
-
678
-    /**
679
-     * User/group search (autocompletion)
680
-     *
681
-     * @param string $search Search string
682
-     * @param int    $mode   Search mode
683
-     *
684
-     * @return array Users/Groups list
685
-     * @throws Exception
686
-     */
687
-    public function autocomplete($search, $mode)
688
-    {
689
-        $ac = new kolab_file_autocomplete($this);
690
-
691
-        $result = $ac->search($search, $mode & file_storage::SEARCH_GROUP);
692
-
693
-        if ($result === false) {
694
-            throw new Exception("Failed to search users", file_storage::ERROR);
695
-        }
696
-
697
-        return $result;
698
-    }
699
-
700
-    /**
701
-     * Convert file/folder path into a global URI.
702
-     *
703
-     * @param string $path File/folder path
704
-     *
705
-     * @return string URI
706
-     * @throws Exception
707
-     */
708
-    public function path2uri($path)
709
-    {
710
-        $storage   = $this->rc->get_storage();
711
-        $namespace = $storage->get_namespace();
712
-        $separator = $storage->get_hierarchy_delimiter();
713
-        $_path     = str_replace(file_storage::SEPARATOR, $separator, $path);
714
-        $_path     = $this->folder_in($_path);
715
-        $owner     = $this->rc->get_user_name();
716
-
717
-        // find the owner and remove namespace prefix
718
-        foreach (array_filter($namespace) as $type => $ns) {
719
-            foreach ($ns as $root) {
720
-                if (is_array($root) && $root[0] && strpos($_path, $root[0]) === 0) {
721
-                    $path = substr($path, strlen($root[0]));
722
-
723
-                    switch ($type) {
724
-                    case 'shared':
725
-                        // in theory there can be more than one shared root
726
-                        // we add it to dummy user name, so we can revert conversion
727
-                        $owner = "shared({$root[0]})";
728
-                        break;
729
-
730
-                    case 'other':
731
-                        list($user, $path) = explode(file_storage::SEPARATOR, $path, 2);
732
-
733
-                        if (strpos($user, '@') === false) {
734
-                            $domain = strstr($owner, '@');
735
-                            if (!empty($domain)) {
736
-                                $user .= $domain;
737
-                            }
738
-                        }
739
-
740
-                        $owner = $user;
741
-                        break;
742
-                    }
743
-
744
-                    break 2;
745
-                }
746
-            }
747
-        }
748
-
749
-        return 'imap://' . rawurlencode($owner) . '@' . $storage->options['host']
750
-            . '/' . file_utils::encode_path($path);
751
-    }
752
-
753
-    /**
754
-     * Convert global URI into file/folder path.
755
-     *
756
-     * @param string $uri URI
757
-     *
758
-     * @return string File/folder path
759
-     * @throws Exception
760
-     */
761
-    public function uri2path($uri)
762
-    {
763
-        if (!preg_match('|^imap://([^@]+)@([^/]+)/(.*)$|', $uri, $matches)) {
764
-            throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR);
765
-        }
766
-
767
-        $storage   = $this->rc->get_storage();
768
-        $separator = $storage->get_hierarchy_delimiter();
769
-        $owner     = $this->rc->get_user_name();
770
-
771
-        $user = rawurldecode($matches[1]);
772
-        $path = file_utils::decode_path($matches[3]);
773
-
774
-        if (strpos($path, '&') !== false) {
775
-            $path = rcube_charset::convert($path, 'UTF7-IMAP', RCUBE_CHARSET);
776
-        }
777
-
778
-        // personal namespace
779
-        if ($user == $owner) {
780
-            // do nothing
781
-            // Note: that might not work if personal namespace uses e.g. INBOX/ prefix.
782
-        }
783
-        // shared namespace
784
-        else if (preg_match('/^shared\((.*)\)$/', $user, $matches)) {
785
-            $path = $matches[1] . $path;
786
-        }
787
-        // other users namespace
788
-        else {
789
-            $namespace = $storage->get_namespace('other');
790
-
791
-            list($local, $domain) = explode('@', $user);
792
-
793
-            // here we assume there's only one other users namespace root
794
-            $path = $namespace[0][0] . $local . file_storage::SEPARATOR . $path;
795
-        }
796
-
797
-        return $path;
798
-    }
799
-
800
-    /**
801
-     * Get files from a folder (with performance fix)
802
-     */
803
-    protected function get_files($folder, $filter, $all = true, $fast_mode = true)
804
-    {
805
-        if (!($folder instanceof kolab_storage_folder)) {
806
-            $folder = $this->get_folder_object($folder);
807
-        }
808
-
809
-        // for better performance it's good to assume max. number of records
810
-        $folder->set_order_and_limit(null, $all ? 0 : 1);
811
-
812
-        return $folder->select($filter, $fast_mode);
813
-    }
814
-
815
-    /**
816
      * Get file object.
817
      *
818
      * @param string               $file_name Name of a file (with folder path)
819
      * @param kolab_storage_folder $folder    Reference to folder object
820
-     * @param bool                 $cache     Use internal cache
821
-     * @param bool                 $fast_mode Return limited list of file attributes
822
      *
823
      * @return array File data
824
      * @throws Exception
825
      */
826
-    protected function get_file_object(&$file_name, &$folder = null, $cache = false, $fast_mode = false)
827
+    protected function get_file_object(&$file_name, &$folder = null)
828
     {
829
-        $original_name = $file_name;
830
-
831
         // extract file path and file name
832
         $path        = explode(file_storage::SEPARATOR, $file_name);
833
         $file_name   = array_pop($path);
834
@@ -1507,41 +1041,14 @@
835
             throw new Exception("Missing folder name", file_storage::ERROR);
836
         }
837
 
838
-        $folder = $this->get_folder_object($this->folder_in($folder_name));
839
-
840
-        if ($cache && !empty($this->icache[$original_name])) {
841
-            return $this->icache[$original_name];
842
-        }
843
-
844
-        $filter = array(
845
-            // array('type', '=', 'file'),
846
+        // get folder object
847
+        $folder = $this->get_folder_object($folder_name);
848
+        $files  = $folder->select(array(
849
+            array('type', '=', 'file'),
850
             array('filename', '=', $file_name)
851
-        );
852
-
853
-        $files = $this->get_files($folder, $filter, false, $fast_mode);
854
-        $file  = $files[0];
855
-
856
-        if ($cache) {
857
-            $this->icache[$original_name] = $file;
858
-        }
859
-
860
-        return $file;
861
-    }
862
+        ));
863
 
864
-    /**
865
-     * Get file object.
866
-     *
867
-     * @param string               $file_name Name of a file (with folder path)
868
-     * @param kolab_storage_folder $folder    Reference to folder object
869
-     * @param bool                 $cache     Use internal cache
870
-     *
871
-     * @return array File data
872
-     * @throws Exception
873
-     * @see self::get_file_object()
874
-     */
875
-    protected function get_file_object_fast(&$file_name, &$folder = null, $cache = false)
876
-    {
877
-        return $this->get_file_object($file_name, $folder, $cache, true);
878
+        return $files[0];
879
     }
880
 
881
     /**
882
@@ -1554,7 +1061,7 @@
883
      */
884
     protected function get_folder_object($folder_name)
885
     {
886
-        if (!is_string($folder_name) || $folder_name === '') {
887
+        if ($folder_name === null || $folder_name === '') {
888
             throw new Exception("Missing folder name", file_storage::ERROR);
889
         }
890
 
891
@@ -1562,19 +1069,10 @@
892
             $storage     = $this->rc->get_storage();
893
             $separator   = $storage->get_hierarchy_delimiter();
894
             $folder_name = str_replace(file_storage::SEPARATOR, $separator, $folder_name);
895
-            $imap_name   = $this->folder_in($folder_name);
896
+            $imap_name   = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
897
             $folder      = kolab_storage::get_folder($imap_name, 'file');
898
 
899
             if (!$folder || !$folder->valid) {
900
-                $error = $folder->get_error();
901
-
902
-                if ($error === kolab_storage::ERROR_IMAP_CONN || $error === kolab_storage::ERROR_CACHE_DB) {
903
-                    throw new Exception("The storage is temporarily unavailable.", file_storage::ERROR_UNAVAILABLE);
904
-                }
905
-                else if ($error === kolab_storage::ERROR_NO_PERMISSION) {
906
-                    throw new Exception("Storage error. Access not permitted", file_storage::ERROR_FORBIDDEN);
907
-                }
908
-
909
                 throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
910
             }
911
 
912
@@ -1589,10 +1087,6 @@
913
      */
914
     protected function from_file_object($file)
915
     {
916
-        if (isset($file['filename']) && !$file['name']) {
917
-            $file['name'] = $file['filename'];
918
-        }
919
-
920
         if (empty($file['_attachments'])) {
921
             return $file;
922
         }
923
@@ -1635,44 +1129,99 @@
924
         return $file;
925
     }
926
 
927
-    /**
928
-     * Initializes file_locks object
929
-     */
930
-    protected function init_lock_db()
931
+    protected function uri2resource($uri)
932
     {
933
-        if (!$this->lock_db) {
934
-            $this->lock_db = new file_locks;
935
+        $storage   = $this->rc->get_storage();
936
+        $namespace = $storage->get_namespace();
937
+        $separator = $storage->get_hierarchy_delimiter();
938
+        $uri       = str_replace(file_storage::SEPARATOR, $separator, $uri);
939
+        $owner     = $this->rc->get_user_name();
940
+
941
+        // find the owner and remove namespace prefix
942
+        foreach ($namespace as $type => $ns) {
943
+            foreach ($ns as $root) {
944
+                if (is_array($root) && $root[0] && strpos($uri, $root[0]) === 0) {
945
+                    $uri = substr($uri, strlen($root[0]));
946
+
947
+                    switch ($type) {
948
+                    case 'shared':
949
+                        // in theory there can be more than one shared root
950
+                        // we add it to dummy user name, so we can revert conversion
951
+                        $owner = "shared({$root[0]})";
952
+                        break;
953
+
954
+                    case 'other':
955
+                        list($user, $uri) = explode($separator, $uri, 2);
956
+
957
+                        if (strpos($user, '@') === false) {
958
+                            $domain = strstr($owner, '@');
959
+                            if (!empty($domain)) {
960
+                                $user .= $domain;
961
+                            }
962
+                        }
963
+
964
+                        $owner = $user;
965
+                        break;
966
+                    }
967
+
968
+                    break 2;
969
+                }
970
+            }
971
         }
972
+
973
+        // convert to imap charset (to be safe to store in DB)
974
+        $uri = rcube_charset::convert($uri, RCUBE_CHARSET, 'UTF7-IMAP');
975
+
976
+        return 'imap://' . urlencode($owner) . '@' . $storage->options['host'] . '/' . $uri;
977
     }
978
 
979
-    /**
980
-     * Apply any conversion on folder name input
981
-     */
982
-    protected function folder_in($folder_name)
983
+    protected function resource2uri($resource)
984
     {
985
-        $folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
986
+        if (!preg_match('|^imap://([^@]+)@([^/]+)/(.*)$|', $resource, $matches)) {
987
+            throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR);
988
+        }
989
+
990
+        $storage   = $this->rc->get_storage();
991
+        $separator = $storage->get_hierarchy_delimiter();
992
+        $owner     = $this->rc->get_user_name();
993
+
994
+        $user = urldecode($matches[1]);
995
+        $uri  = $matches[3];
996
+
997
+        // convert from imap charset (to be safe to store in DB)
998
+        $uri = rcube_charset::convert($uri, 'UTF7-IMAP', RCUBE_CHARSET);
999
+
1000
+        // personal namespace
1001
+        if ($user == $owner) {
1002
+            // do nothing
1003
+            // Note: that might not work if personal namespace uses e.g. INBOX/ prefix.
1004
+        }
1005
+        // shared namespace
1006
+        else if (preg_match('/^shared\((.*)\)$/', $user, $matches)) {
1007
+            $uri = $matches[1] . $uri;
1008
+        }
1009
+        // other users namespace
1010
+        else {
1011
+            $namespace = $storage->get_namespace('other');
1012
 
1013
-        $plugin = $this->rc->plugins->exec_hook('folder_mod',
1014
-            array('folder' => $folder_name, 'dir' => 'in'));
1015
+            list($local, $domain) = explode('@', $user);
1016
 
1017
-        return $plugin['folder'];
1018
+            // here we assume there's only one other users namespace root
1019
+            $uri = $namespace[0][0] . $local . $separator . $uri;
1020
+        }
1021
+
1022
+        $uri = str_replace($separator, file_storage::SEPARATOR, $uri);
1023
+
1024
+        return $uri;
1025
     }
1026
 
1027
     /**
1028
-     * Apply any conversion on folder name output
1029
-     *
1030
-     * For example plugins can replace "/Outher Users/jane.doe/"
1031
-     * with "/Other Users/Doe, Jane (jane.doe)/"
1032
+     * Initializes file_locks object
1033
      */
1034
-    protected function folder_out($folder_name)
1035
+    protected function init_lock_db()
1036
     {
1037
-        $plugin = $this->rc->plugins->exec_hook('folder_mod',
1038
-            array('folder' => $folder_name, 'dir' => 'out'));
1039
-
1040
-        if (strpos($plugin['folder'], '&') !== false) {
1041
-            $plugin['folder'] = rcube_charset::convert($plugin['folder'], 'UTF7-IMAP', RCUBE_CHARSET);
1042
+        if (!$this->lock_db) {
1043
+            $this->lock_db = new file_locks;
1044
         }
1045
-
1046
-        return $plugin['folder'];
1047
     }
1048
 }
1049
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/LICENSE Added
663
 
1
@@ -0,0 +1,661 @@
2
+                    GNU AFFERO GENERAL PUBLIC LICENSE
3
+                       Version 3, 19 November 2007
4
+
5
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
6
+ Everyone is permitted to copy and distribute verbatim copies
7
+ of this license document, but changing it is not allowed.
8
+
9
+                            Preamble
10
+
11
+  The GNU Affero General Public License is a free, copyleft license for
12
+software and other kinds of works, specifically designed to ensure
13
+cooperation with the community in the case of network server software.
14
+
15
+  The licenses for most software and other practical works are designed
16
+to take away your freedom to share and change the works.  By contrast,
17
+our General Public Licenses are intended to guarantee your freedom to
18
+share and change all versions of a program--to make sure it remains free
19
+software for all its users.
20
+
21
+  When we speak of free software, we are referring to freedom, not
22
+price.  Our General Public Licenses are designed to make sure that you
23
+have the freedom to distribute copies of free software (and charge for
24
+them if you wish), that you receive source code or can get it if you
25
+want it, that you can change the software or use pieces of it in new
26
+free programs, and that you know you can do these things.
27
+
28
+  Developers that use our General Public Licenses protect your rights
29
+with two steps: (1) assert copyright on the software, and (2) offer
30
+you this License which gives you legal permission to copy, distribute
31
+and/or modify the software.
32
+
33
+  A secondary benefit of defending all users' freedom is that
34
+improvements made in alternate versions of the program, if they
35
+receive widespread use, become available for other developers to
36
+incorporate.  Many developers of free software are heartened and
37
+encouraged by the resulting cooperation.  However, in the case of
38
+software used on network servers, this result may fail to come about.
39
+The GNU General Public License permits making a modified version and
40
+letting the public access it on a server without ever releasing its
41
+source code to the public.
42
+
43
+  The GNU Affero General Public License is designed specifically to
44
+ensure that, in such cases, the modified source code becomes available
45
+to the community.  It requires the operator of a network server to
46
+provide the source code of the modified version running there to the
47
+users of that server.  Therefore, public use of a modified version, on
48
+a publicly accessible server, gives the public access to the source
49
+code of the modified version.
50
+
51
+  An older license, called the Affero General Public License and
52
+published by Affero, was designed to accomplish similar goals.  This is
53
+a different license, not a version of the Affero GPL, but Affero has
54
+released a new version of the Affero GPL which permits relicensing under
55
+this license.
56
+
57
+  The precise terms and conditions for copying, distribution and
58
+modification follow.
59
+
60
+                       TERMS AND CONDITIONS
61
+
62
+  0. Definitions.
63
+
64
+  "This License" refers to version 3 of the GNU Affero General Public License.
65
+
66
+  "Copyright" also means copyright-like laws that apply to other kinds of
67
+works, such as semiconductor masks.
68
+
69
+  "The Program" refers to any copyrightable work licensed under this
70
+License.  Each licensee is addressed as "you".  "Licensees" and
71
+"recipients" may be individuals or organizations.
72
+
73
+  To "modify" a work means to copy from or adapt all or part of the work
74
+in a fashion requiring copyright permission, other than the making of an
75
+exact copy.  The resulting work is called a "modified version" of the
76
+earlier work or a work "based on" the earlier work.
77
+
78
+  A "covered work" means either the unmodified Program or a work based
79
+on the Program.
80
+
81
+  To "propagate" a work means to do anything with it that, without
82
+permission, would make you directly or secondarily liable for
83
+infringement under applicable copyright law, except executing it on a
84
+computer or modifying a private copy.  Propagation includes copying,
85
+distribution (with or without modification), making available to the
86
+public, and in some countries other activities as well.
87
+
88
+  To "convey" a work means any kind of propagation that enables other
89
+parties to make or receive copies.  Mere interaction with a user through
90
+a computer network, with no transfer of a copy, is not conveying.
91
+
92
+  An interactive user interface displays "Appropriate Legal Notices"
93
+to the extent that it includes a convenient and prominently visible
94
+feature that (1) displays an appropriate copyright notice, and (2)
95
+tells the user that there is no warranty for the work (except to the
96
+extent that warranties are provided), that licensees may convey the
97
+work under this License, and how to view a copy of this License.  If
98
+the interface presents a list of user commands or options, such as a
99
+menu, a prominent item in the list meets this criterion.
100
+
101
+  1. Source Code.
102
+
103
+  The "source code" for a work means the preferred form of the work
104
+for making modifications to it.  "Object code" means any non-source
105
+form of a work.
106
+
107
+  A "Standard Interface" means an interface that either is an official
108
+standard defined by a recognized standards body, or, in the case of
109
+interfaces specified for a particular programming language, one that
110
+is widely used among developers working in that language.
111
+
112
+  The "System Libraries" of an executable work include anything, other
113
+than the work as a whole, that (a) is included in the normal form of
114
+packaging a Major Component, but which is not part of that Major
115
+Component, and (b) serves only to enable use of the work with that
116
+Major Component, or to implement a Standard Interface for which an
117
+implementation is available to the public in source code form.  A
118
+"Major Component", in this context, means a major essential component
119
+(kernel, window system, and so on) of the specific operating system
120
+(if any) on which the executable work runs, or a compiler used to
121
+produce the work, or an object code interpreter used to run it.
122
+
123
+  The "Corresponding Source" for a work in object code form means all
124
+the source code needed to generate, install, and (for an executable
125
+work) run the object code and to modify the work, including scripts to
126
+control those activities.  However, it does not include the work's
127
+System Libraries, or general-purpose tools or generally available free
128
+programs which are used unmodified in performing those activities but
129
+which are not part of the work.  For example, Corresponding Source
130
+includes interface definition files associated with source files for
131
+the work, and the source code for shared libraries and dynamically
132
+linked subprograms that the work is specifically designed to require,
133
+such as by intimate data communication or control flow between those
134
+subprograms and other parts of the work.
135
+
136
+  The Corresponding Source need not include anything that users
137
+can regenerate automatically from other parts of the Corresponding
138
+Source.
139
+
140
+  The Corresponding Source for a work in source code form is that
141
+same work.
142
+
143
+  2. Basic Permissions.
144
+
145
+  All rights granted under this License are granted for the term of
146
+copyright on the Program, and are irrevocable provided the stated
147
+conditions are met.  This License explicitly affirms your unlimited
148
+permission to run the unmodified Program.  The output from running a
149
+covered work is covered by this License only if the output, given its
150
+content, constitutes a covered work.  This License acknowledges your
151
+rights of fair use or other equivalent, as provided by copyright law.
152
+
153
+  You may make, run and propagate covered works that you do not
154
+convey, without conditions so long as your license otherwise remains
155
+in force.  You may convey covered works to others for the sole purpose
156
+of having them make modifications exclusively for you, or provide you
157
+with facilities for running those works, provided that you comply with
158
+the terms of this License in conveying all material for which you do
159
+not control copyright.  Those thus making or running the covered works
160
+for you must do so exclusively on your behalf, under your direction
161
+and control, on terms that prohibit them from making any copies of
162
+your copyrighted material outside their relationship with you.
163
+
164
+  Conveying under any other circumstances is permitted solely under
165
+the conditions stated below.  Sublicensing is not allowed; section 10
166
+makes it unnecessary.
167
+
168
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
169
+
170
+  No covered work shall be deemed part of an effective technological
171
+measure under any applicable law fulfilling obligations under article
172
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
173
+similar laws prohibiting or restricting circumvention of such
174
+measures.
175
+
176
+  When you convey a covered work, you waive any legal power to forbid
177
+circumvention of technological measures to the extent such circumvention
178
+is effected by exercising rights under this License with respect to
179
+the covered work, and you disclaim any intention to limit operation or
180
+modification of the work as a means of enforcing, against the work's
181
+users, your or third parties' legal rights to forbid circumvention of
182
+technological measures.
183
+
184
+  4. Conveying Verbatim Copies.
185
+
186
+  You may convey verbatim copies of the Program's source code as you
187
+receive it, in any medium, provided that you conspicuously and
188
+appropriately publish on each copy an appropriate copyright notice;
189
+keep intact all notices stating that this License and any
190
+non-permissive terms added in accord with section 7 apply to the code;
191
+keep intact all notices of the absence of any warranty; and give all
192
+recipients a copy of this License along with the Program.
193
+
194
+  You may charge any price or no price for each copy that you convey,
195
+and you may offer support or warranty protection for a fee.
196
+
197
+  5. Conveying Modified Source Versions.
198
+
199
+  You may convey a work based on the Program, or the modifications to
200
+produce it from the Program, in the form of source code under the
201
+terms of section 4, provided that you also meet all of these conditions:
202
+
203
+    a) The work must carry prominent notices stating that you modified
204
+    it, and giving a relevant date.
205
+
206
+    b) The work must carry prominent notices stating that it is
207
+    released under this License and any conditions added under section
208
+    7.  This requirement modifies the requirement in section 4 to
209
+    "keep intact all notices".
210
+
211
+    c) You must license the entire work, as a whole, under this
212
+    License to anyone who comes into possession of a copy.  This
213
+    License will therefore apply, along with any applicable section 7
214
+    additional terms, to the whole of the work, and all its parts,
215
+    regardless of how they are packaged.  This License gives no
216
+    permission to license the work in any other way, but it does not
217
+    invalidate such permission if you have separately received it.
218
+
219
+    d) If the work has interactive user interfaces, each must display
220
+    Appropriate Legal Notices; however, if the Program has interactive
221
+    interfaces that do not display Appropriate Legal Notices, your
222
+    work need not make them do so.
223
+
224
+  A compilation of a covered work with other separate and independent
225
+works, which are not by their nature extensions of the covered work,
226
+and which are not combined with it such as to form a larger program,
227
+in or on a volume of a storage or distribution medium, is called an
228
+"aggregate" if the compilation and its resulting copyright are not
229
+used to limit the access or legal rights of the compilation's users
230
+beyond what the individual works permit.  Inclusion of a covered work
231
+in an aggregate does not cause this License to apply to the other
232
+parts of the aggregate.
233
+
234
+  6. Conveying Non-Source Forms.
235
+
236
+  You may convey a covered work in object code form under the terms
237
+of sections 4 and 5, provided that you also convey the
238
+machine-readable Corresponding Source under the terms of this License,
239
+in one of these ways:
240
+
241
+    a) Convey the object code in, or embodied in, a physical product
242
+    (including a physical distribution medium), accompanied by the
243
+    Corresponding Source fixed on a durable physical medium
244
+    customarily used for software interchange.
245
+
246
+    b) Convey the object code in, or embodied in, a physical product
247
+    (including a physical distribution medium), accompanied by a
248
+    written offer, valid for at least three years and valid for as
249
+    long as you offer spare parts or customer support for that product
250
+    model, to give anyone who possesses the object code either (1) a
251
+    copy of the Corresponding Source for all the software in the
252
+    product that is covered by this License, on a durable physical
253
+    medium customarily used for software interchange, for a price no
254
+    more than your reasonable cost of physically performing this
255
+    conveying of source, or (2) access to copy the
256
+    Corresponding Source from a network server at no charge.
257
+
258
+    c) Convey individual copies of the object code with a copy of the
259
+    written offer to provide the Corresponding Source.  This
260
+    alternative is allowed only occasionally and noncommercially, and
261
+    only if you received the object code with such an offer, in accord
262
+    with subsection 6b.
263
+
264
+    d) Convey the object code by offering access from a designated
265
+    place (gratis or for a charge), and offer equivalent access to the
266
+    Corresponding Source in the same way through the same place at no
267
+    further charge.  You need not require recipients to copy the
268
+    Corresponding Source along with the object code.  If the place to
269
+    copy the object code is a network server, the Corresponding Source
270
+    may be on a different server (operated by you or a third party)
271
+    that supports equivalent copying facilities, provided you maintain
272
+    clear directions next to the object code saying where to find the
273
+    Corresponding Source.  Regardless of what server hosts the
274
+    Corresponding Source, you remain obligated to ensure that it is
275
+    available for as long as needed to satisfy these requirements.
276
+
277
+    e) Convey the object code using peer-to-peer transmission, provided
278
+    you inform other peers where the object code and Corresponding
279
+    Source of the work are being offered to the general public at no
280
+    charge under subsection 6d.
281
+
282
+  A separable portion of the object code, whose source code is excluded
283
+from the Corresponding Source as a System Library, need not be
284
+included in conveying the object code work.
285
+
286
+  A "User Product" is either (1) a "consumer product", which means any
287
+tangible personal property which is normally used for personal, family,
288
+or household purposes, or (2) anything designed or sold for incorporation
289
+into a dwelling.  In determining whether a product is a consumer product,
290
+doubtful cases shall be resolved in favor of coverage.  For a particular
291
+product received by a particular user, "normally used" refers to a
292
+typical or common use of that class of product, regardless of the status
293
+of the particular user or of the way in which the particular user
294
+actually uses, or expects or is expected to use, the product.  A product
295
+is a consumer product regardless of whether the product has substantial
296
+commercial, industrial or non-consumer uses, unless such uses represent
297
+the only significant mode of use of the product.
298
+
299
+  "Installation Information" for a User Product means any methods,
300
+procedures, authorization keys, or other information required to install
301
+and execute modified versions of a covered work in that User Product from
302
+a modified version of its Corresponding Source.  The information must
303
+suffice to ensure that the continued functioning of the modified object
304
+code is in no case prevented or interfered with solely because
305
+modification has been made.
306
+
307
+  If you convey an object code work under this section in, or with, or
308
+specifically for use in, a User Product, and the conveying occurs as
309
+part of a transaction in which the right of possession and use of the
310
+User Product is transferred to the recipient in perpetuity or for a
311
+fixed term (regardless of how the transaction is characterized), the
312
+Corresponding Source conveyed under this section must be accompanied
313
+by the Installation Information.  But this requirement does not apply
314
+if neither you nor any third party retains the ability to install
315
+modified object code on the User Product (for example, the work has
316
+been installed in ROM).
317
+
318
+  The requirement to provide Installation Information does not include a
319
+requirement to continue to provide support service, warranty, or updates
320
+for a work that has been modified or installed by the recipient, or for
321
+the User Product in which it has been modified or installed.  Access to a
322
+network may be denied when the modification itself materially and
323
+adversely affects the operation of the network or violates the rules and
324
+protocols for communication across the network.
325
+
326
+  Corresponding Source conveyed, and Installation Information provided,
327
+in accord with this section must be in a format that is publicly
328
+documented (and with an implementation available to the public in
329
+source code form), and must require no special password or key for
330
+unpacking, reading or copying.
331
+
332
+  7. Additional Terms.
333
+
334
+  "Additional permissions" are terms that supplement the terms of this
335
+License by making exceptions from one or more of its conditions.
336
+Additional permissions that are applicable to the entire Program shall
337
+be treated as though they were included in this License, to the extent
338
+that they are valid under applicable law.  If additional permissions
339
+apply only to part of the Program, that part may be used separately
340
+under those permissions, but the entire Program remains governed by
341
+this License without regard to the additional permissions.
342
+
343
+  When you convey a copy of a covered work, you may at your option
344
+remove any additional permissions from that copy, or from any part of
345
+it.  (Additional permissions may be written to require their own
346
+removal in certain cases when you modify the work.)  You may place
347
+additional permissions on material, added by you to a covered work,
348
+for which you have or can give appropriate copyright permission.
349
+
350
+  Notwithstanding any other provision of this License, for material you
351
+add to a covered work, you may (if authorized by the copyright holders of
352
+that material) supplement the terms of this License with terms:
353
+
354
+    a) Disclaiming warranty or limiting liability differently from the
355
+    terms of sections 15 and 16 of this License; or
356
+
357
+    b) Requiring preservation of specified reasonable legal notices or
358
+    author attributions in that material or in the Appropriate Legal
359
+    Notices displayed by works containing it; or
360
+
361
+    c) Prohibiting misrepresentation of the origin of that material, or
362
+    requiring that modified versions of such material be marked in
363
+    reasonable ways as different from the original version; or
364
+
365
+    d) Limiting the use for publicity purposes of names of licensors or
366
+    authors of the material; or
367
+
368
+    e) Declining to grant rights under trademark law for use of some
369
+    trade names, trademarks, or service marks; or
370
+
371
+    f) Requiring indemnification of licensors and authors of that
372
+    material by anyone who conveys the material (or modified versions of
373
+    it) with contractual assumptions of liability to the recipient, for
374
+    any liability that these contractual assumptions directly impose on
375
+    those licensors and authors.
376
+
377
+  All other non-permissive additional terms are considered "further
378
+restrictions" within the meaning of section 10.  If the Program as you
379
+received it, or any part of it, contains a notice stating that it is
380
+governed by this License along with a term that is a further
381
+restriction, you may remove that term.  If a license document contains
382
+a further restriction but permits relicensing or conveying under this
383
+License, you may add to a covered work material governed by the terms
384
+of that license document, provided that the further restriction does
385
+not survive such relicensing or conveying.
386
+
387
+  If you add terms to a covered work in accord with this section, you
388
+must place, in the relevant source files, a statement of the
389
+additional terms that apply to those files, or a notice indicating
390
+where to find the applicable terms.
391
+
392
+  Additional terms, permissive or non-permissive, may be stated in the
393
+form of a separately written license, or stated as exceptions;
394
+the above requirements apply either way.
395
+
396
+  8. Termination.
397
+
398
+  You may not propagate or modify a covered work except as expressly
399
+provided under this License.  Any attempt otherwise to propagate or
400
+modify it is void, and will automatically terminate your rights under
401
+this License (including any patent licenses granted under the third
402
+paragraph of section 11).
403
+
404
+  However, if you cease all violation of this License, then your
405
+license from a particular copyright holder is reinstated (a)
406
+provisionally, unless and until the copyright holder explicitly and
407
+finally terminates your license, and (b) permanently, if the copyright
408
+holder fails to notify you of the violation by some reasonable means
409
+prior to 60 days after the cessation.
410
+
411
+  Moreover, your license from a particular copyright holder is
412
+reinstated permanently if the copyright holder notifies you of the
413
+violation by some reasonable means, this is the first time you have
414
+received notice of violation of this License (for any work) from that
415
+copyright holder, and you cure the violation prior to 30 days after
416
+your receipt of the notice.
417
+
418
+  Termination of your rights under this section does not terminate the
419
+licenses of parties who have received copies or rights from you under
420
+this License.  If your rights have been terminated and not permanently
421
+reinstated, you do not qualify to receive new licenses for the same
422
+material under section 10.
423
+
424
+  9. Acceptance Not Required for Having Copies.
425
+
426
+  You are not required to accept this License in order to receive or
427
+run a copy of the Program.  Ancillary propagation of a covered work
428
+occurring solely as a consequence of using peer-to-peer transmission
429
+to receive a copy likewise does not require acceptance.  However,
430
+nothing other than this License grants you permission to propagate or
431
+modify any covered work.  These actions infringe copyright if you do
432
+not accept this License.  Therefore, by modifying or propagating a
433
+covered work, you indicate your acceptance of this License to do so.
434
+
435
+  10. Automatic Licensing of Downstream Recipients.
436
+
437
+  Each time you convey a covered work, the recipient automatically
438
+receives a license from the original licensors, to run, modify and
439
+propagate that work, subject to this License.  You are not responsible
440
+for enforcing compliance by third parties with this License.
441
+
442
+  An "entity transaction" is a transaction transferring control of an
443
+organization, or substantially all assets of one, or subdividing an
444
+organization, or merging organizations.  If propagation of a covered
445
+work results from an entity transaction, each party to that
446
+transaction who receives a copy of the work also receives whatever
447
+licenses to the work the party's predecessor in interest had or could
448
+give under the previous paragraph, plus a right to possession of the
449
+Corresponding Source of the work from the predecessor in interest, if
450
+the predecessor has it or can get it with reasonable efforts.
451
+
452
+  You may not impose any further restrictions on the exercise of the
453
+rights granted or affirmed under this License.  For example, you may
454
+not impose a license fee, royalty, or other charge for exercise of
455
+rights granted under this License, and you may not initiate litigation
456
+(including a cross-claim or counterclaim in a lawsuit) alleging that
457
+any patent claim is infringed by making, using, selling, offering for
458
+sale, or importing the Program or any portion of it.
459
+
460
+  11. Patents.
461
+
462
+  A "contributor" is a copyright holder who authorizes use under this
463
+License of the Program or a work on which the Program is based.  The
464
+work thus licensed is called the contributor's "contributor version".
465
+
466
+  A contributor's "essential patent claims" are all patent claims
467
+owned or controlled by the contributor, whether already acquired or
468
+hereafter acquired, that would be infringed by some manner, permitted
469
+by this License, of making, using, or selling its contributor version,
470
+but do not include claims that would be infringed only as a
471
+consequence of further modification of the contributor version.  For
472
+purposes of this definition, "control" includes the right to grant
473
+patent sublicenses in a manner consistent with the requirements of
474
+this License.
475
+
476
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
477
+patent license under the contributor's essential patent claims, to
478
+make, use, sell, offer for sale, import and otherwise run, modify and
479
+propagate the contents of its contributor version.
480
+
481
+  In the following three paragraphs, a "patent license" is any express
482
+agreement or commitment, however denominated, not to enforce a patent
483
+(such as an express permission to practice a patent or covenant not to
484
+sue for patent infringement).  To "grant" such a patent license to a
485
+party means to make such an agreement or commitment not to enforce a
486
+patent against the party.
487
+
488
+  If you convey a covered work, knowingly relying on a patent license,
489
+and the Corresponding Source of the work is not available for anyone
490
+to copy, free of charge and under the terms of this License, through a
491
+publicly available network server or other readily accessible means,
492
+then you must either (1) cause the Corresponding Source to be so
493
+available, or (2) arrange to deprive yourself of the benefit of the
494
+patent license for this particular work, or (3) arrange, in a manner
495
+consistent with the requirements of this License, to extend the patent
496
+license to downstream recipients.  "Knowingly relying" means you have
497
+actual knowledge that, but for the patent license, your conveying the
498
+covered work in a country, or your recipient's use of the covered work
499
+in a country, would infringe one or more identifiable patents in that
500
+country that you have reason to believe are valid.
501
+
502
+  If, pursuant to or in connection with a single transaction or
503
+arrangement, you convey, or propagate by procuring conveyance of, a
504
+covered work, and grant a patent license to some of the parties
505
+receiving the covered work authorizing them to use, propagate, modify
506
+or convey a specific copy of the covered work, then the patent license
507
+you grant is automatically extended to all recipients of the covered
508
+work and works based on it.
509
+
510
+  A patent license is "discriminatory" if it does not include within
511
+the scope of its coverage, prohibits the exercise of, or is
512
+conditioned on the non-exercise of one or more of the rights that are
513
+specifically granted under this License.  You may not convey a covered
514
+work if you are a party to an arrangement with a third party that is
515
+in the business of distributing software, under which you make payment
516
+to the third party based on the extent of your activity of conveying
517
+the work, and under which the third party grants, to any of the
518
+parties who would receive the covered work from you, a discriminatory
519
+patent license (a) in connection with copies of the covered work
520
+conveyed by you (or copies made from those copies), or (b) primarily
521
+for and in connection with specific products or compilations that
522
+contain the covered work, unless you entered into that arrangement,
523
+or that patent license was granted, prior to 28 March 2007.
524
+
525
+  Nothing in this License shall be construed as excluding or limiting
526
+any implied license or other defenses to infringement that may
527
+otherwise be available to you under applicable patent law.
528
+
529
+  12. No Surrender of Others' Freedom.
530
+
531
+  If conditions are imposed on you (whether by court order, agreement or
532
+otherwise) that contradict the conditions of this License, they do not
533
+excuse you from the conditions of this License.  If you cannot convey a
534
+covered work so as to satisfy simultaneously your obligations under this
535
+License and any other pertinent obligations, then as a consequence you may
536
+not convey it at all.  For example, if you agree to terms that obligate you
537
+to collect a royalty for further conveying from those to whom you convey
538
+the Program, the only way you could satisfy both those terms and this
539
+License would be to refrain entirely from conveying the Program.
540
+
541
+  13. Remote Network Interaction; Use with the GNU General Public License.
542
+
543
+  Notwithstanding any other provision of this License, if you modify the
544
+Program, your modified version must prominently offer all users
545
+interacting with it remotely through a computer network (if your version
546
+supports such interaction) an opportunity to receive the Corresponding
547
+Source of your version by providing access to the Corresponding Source
548
+from a network server at no charge, through some standard or customary
549
+means of facilitating copying of software.  This Corresponding Source
550
+shall include the Corresponding Source for any work covered by version 3
551
+of the GNU General Public License that is incorporated pursuant to the
552
+following paragraph.
553
+
554
+  Notwithstanding any other provision of this License, you have
555
+permission to link or combine any covered work with a work licensed
556
+under version 3 of the GNU General Public License into a single
557
+combined work, and to convey the resulting work.  The terms of this
558
+License will continue to apply to the part which is the covered work,
559
+but the work with which it is combined will remain governed by version
560
+3 of the GNU General Public License.
561
+
562
+  14. Revised Versions of this License.
563
+
564
+  The Free Software Foundation may publish revised and/or new versions of
565
+the GNU Affero General Public License from time to time.  Such new versions
566
+will be similar in spirit to the present version, but may differ in detail to
567
+address new problems or concerns.
568
+
569
+  Each version is given a distinguishing version number.  If the
570
+Program specifies that a certain numbered version of the GNU Affero General
571
+Public License "or any later version" applies to it, you have the
572
+option of following the terms and conditions either of that numbered
573
+version or of any later version published by the Free Software
574
+Foundation.  If the Program does not specify a version number of the
575
+GNU Affero General Public License, you may choose any version ever published
576
+by the Free Software Foundation.
577
+
578
+  If the Program specifies that a proxy can decide which future
579
+versions of the GNU Affero General Public License can be used, that proxy's
580
+public statement of acceptance of a version permanently authorizes you
581
+to choose that version for the Program.
582
+
583
+  Later license versions may give you additional or different
584
+permissions.  However, no additional obligations are imposed on any
585
+author or copyright holder as a result of your choosing to follow a
586
+later version.
587
+
588
+  15. Disclaimer of Warranty.
589
+
590
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
591
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
592
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
593
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
594
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
595
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
596
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
597
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
598
+
599
+  16. Limitation of Liability.
600
+
601
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
602
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
603
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
604
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
605
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
606
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
607
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
608
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
609
+SUCH DAMAGES.
610
+
611
+  17. Interpretation of Sections 15 and 16.
612
+
613
+  If the disclaimer of warranty and limitation of liability provided
614
+above cannot be given local legal effect according to their terms,
615
+reviewing courts shall apply local law that most closely approximates
616
+an absolute waiver of all civil liability in connection with the
617
+Program, unless a warranty or assumption of liability accompanies a
618
+copy of the Program in return for a fee.
619
+
620
+                     END OF TERMS AND CONDITIONS
621
+
622
+            How to Apply These Terms to Your New Programs
623
+
624
+  If you develop a new program, and you want it to be of the greatest
625
+possible use to the public, the best way to achieve this is to make it
626
+free software which everyone can redistribute and change under these terms.
627
+
628
+  To do so, attach the following notices to the program.  It is safest
629
+to attach them to the start of each source file to most effectively
630
+state the exclusion of warranty; and each file should have at least
631
+the "copyright" line and a pointer to where the full notice is found.
632
+
633
+    <one line to give the program's name and a brief idea of what it does.>
634
+    Copyright (C) <year>  <name of author>
635
+
636
+    This program is free software: you can redistribute it and/or modify
637
+    it under the terms of the GNU Affero General Public License as published by
638
+    the Free Software Foundation, either version 3 of the License, or
639
+    (at your option) any later version.
640
+
641
+    This program is distributed in the hope that it will be useful,
642
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
643
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
644
+    GNU Affero General Public License for more details.
645
+
646
+    You should have received a copy of the GNU Affero General Public License
647
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
648
+
649
+Also add information on how to contact you by electronic and paper mail.
650
+
651
+  If your software can interact with users remotely through a computer
652
+network, you should also make sure that it provides a way for users to
653
+get its source.  For example, if your program is a web application, its
654
+interface could display a "Source" link that leads users to an archive
655
+of the code.  There are many ways you could offer source, and different
656
+solutions will be better for different programs; see section 13 for the
657
+specific requirements.
658
+
659
+  You should also get your employer (if you work as a programmer) or school,
660
+if any, to sign a "copyright disclaimer" for the program, if necessary.
661
+For more information on this, and how to apply and follow the GNU AGPL, see
662
+<http://www.gnu.org/licenses/>.
663
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/composer.json Added
32
 
1
@@ -0,0 +1,30 @@
2
+{
3
+    "name": "kolab/kolab_auth",
4
+    "type": "roundcube-plugin",
5
+    "description": "Kolab authentication",
6
+    "homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
7
+    "license": "AGPLv3",
8
+    "version": "3.2.2",
9
+    "authors": [
10
+        {
11
+            "name": "Thomas Bruederli",
12
+            "email": "bruederli@kolabsys.com",
13
+            "role": "Lead"
14
+        },
15
+        {
16
+            "name": "Aleksander Machniak",
17
+            "email": "machniak@kolabsys.com",
18
+            "role": "Lead"
19
+        }
20
+    ],
21
+    "repositories": [
22
+        {
23
+            "type": "composer",
24
+            "url": "http://plugins.roundcube.net"
25
+        }
26
+    ],
27
+    "require": {
28
+        "php": ">=5.3.0",
29
+        "roundcube/plugin-installer": ">=0.1.3"
30
+    }
31
+}
32
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/config.inc.php.dist Added
93
 
1
@@ -0,0 +1,91 @@
2
+<?php
3
+
4
+// The id of the LDAP address book (which refers to the $rcmail_config['ldap_public'])
5
+// or complete addressbook definition array.
6
+// --------------------------------------------------------------------
7
+// Note: Multi-domain (hosted) installations can resolve domain aliases
8
+//   by adding following settings in kolab_auth_addressbook spec.:
9
+//
10
+//   'domain_base_dn'   => 'cn=kolab,cn=config',
11
+//   'domain_filter'    => '(&(objectclass=domainrelatedobject)(associateddomain=%s))',
12
+//   'domain_name_attr' => 'associateddomain',
13
+//
14
+//   With this %dc variable in base_dn and groups/base_dn will be
15
+//   replaced with DN string of resolved domain
16
+//---------------------------------------------------------------------
17
+$config['kolab_auth_addressbook'] = '';
18
+
19
+// This will overwrite defined filter
20
+$config['kolab_auth_filter'] = '(&(objectClass=kolabInetOrgPerson)(|(uid=%u)(mail=%fu)(alias=%fu)))';
21
+
22
+// Use this field (from fieldmap configuration) to get authentication ID. Don't use an array here!
23
+$config['kolab_auth_login'] = 'email';
24
+
25
+// Use these fields (from fieldmap configuration) for default identity.
26
+// If the value array contains more than one field, first non-empty will be used
27
+// Note: These aren't LDAP attributes, but field names in config
28
+// Note: If there's more than one email address, as many identities will be created
29
+$config['kolab_auth_name']         = array('name', 'cn');
30
+$config['kolab_auth_email']        = array('email');
31
+$config['kolab_auth_organization'] = array('organization');
32
+
33
+// Role field (from fieldmap configuration)
34
+$config['kolab_auth_role'] = 'role';
35
+
36
+// Template for user names displayed in the UI.
37
+// You can use all attributes from the 'fieldmap' property of the 'kolab_auth_addressbook' configuration
38
+$config['kolab_auth_user_displayname'] = '{name} ({ou})';
39
+
40
+// Login and password of the admin user. Enables "Login As" feature.
41
+$config['kolab_auth_admin_login']    = '';
42
+$config['kolab_auth_admin_password'] = '';
43
+
44
+// Enable audit logging for abuse of administrative privileges.
45
+$config['kolab_auth_auditlog'] = false;
46
+
47
+// As set of rules to define the required rights on the target entry
48
+// which allow an admin user to login as another user (the target).
49
+// The effective rights value refers to either entry level attribute level rights:
50
+//  * entry:[read|add|delete]
51
+//  * attrib:<attribute-name>:[read|write|delete]
52
+$config['kolab_auth_admin_rights'] = array(
53
+    // Roundcube task => required effective right
54
+    'settings'        => 'entry:read',
55
+    'mail'            => 'entry:delete',
56
+    'addressbook'     => 'entry:delete',
57
+    // or use a wildcard entry like this:
58
+    '*'               => 'entry:read',
59
+);
60
+
61
+// Enable plugins on a role-by-role basis. In this example, the 'acl' plugin
62
+// is enabled for people with a 'cn=professional-user,dc=mykolab,dc=ch' role.
63
+//
64
+// Note that this does NOT mean the 'acl' plugin is disabled for other people.
65
+$config['kolab_auth_role_plugins'] = Array(
66
+        'cn=professional-user,dc=mykolab,dc=ch' => Array(
67
+                'acl',
68
+            ),
69
+    );
70
+
71
+// Settings on a role-by-role basis. In this example, the 'htmleditor' setting
72
+// is enabled(1) for people with a 'cn=professional-user,dc=mykolab,dc=ch' role,
73
+// and it cannot be overridden. Sample use-case: disable htmleditor for normal people,
74
+// do not allow the setting to be controlled through the preferences, enable the
75
+// html editor for professional users and allow them to override the setting in
76
+// the preferences.
77
+$config['kolab_auth_role_settings'] = Array(
78
+        'cn=professional-user,dc=mykolab,dc=ch' => Array(
79
+                'htmleditor' => Array(
80
+                        'mode' => 'override',
81
+                        'value' => 1,
82
+                        'allow_override' => true
83
+                    ),
84
+            ),
85
+    );
86
+
87
+// List of LDAP addressbooks (keys of ldap_public configuration array)
88
+// for which base_dn variables (%dc, etc.) will be replaced according to authenticated user DN
89
+// Note: special name '*' for all LDAP addressbooks
90
+$config['kolab_auth_ldap_addressbooks'] = array('*');
91
+
92
+?>
93
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/kolab_auth.php Added
790
 
1
@@ -0,0 +1,788 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab Authentication (based on ldap_authentication plugin)
6
+ *
7
+ * Authenticates on LDAP server, finds canonized authentication ID for IMAP
8
+ * and for new users creates identity based on LDAP information.
9
+ *
10
+ * Supports impersonate feature (login as another user). To use this feature
11
+ * imap_auth_type/smtp_auth_type must be set to DIGEST-MD5 or PLAIN.
12
+ *
13
+ * @version @package_version@
14
+ * @author Aleksander Machniak <machniak@kolabsys.com>
15
+ *
16
+ * Copyright (C) 2011-2013, Kolab Systems AG <contact@kolabsys.com>
17
+ *
18
+ * This program is free software: you can redistribute it and/or modify
19
+ * it under the terms of the GNU Affero General Public License as
20
+ * published by the Free Software Foundation, either version 3 of the
21
+ * License, or (at your option) any later version.
22
+ *
23
+ * This program is distributed in the hope that it will be useful,
24
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
+ * GNU Affero General Public License for more details.
27
+ *
28
+ * You should have received a copy of the GNU Affero General Public License
29
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
30
+ */
31
+
32
+class kolab_auth extends rcube_plugin
33
+{
34
+    static $ldap;
35
+    private $username;
36
+    private $data = array();
37
+
38
+    public function init()
39
+    {
40
+        $rcmail = rcube::get_instance();
41
+
42
+        $this->load_config();
43
+
44
+        $this->add_hook('authenticate', array($this, 'authenticate'));
45
+        $this->add_hook('startup', array($this, 'startup'));
46
+        $this->add_hook('user_create', array($this, 'user_create'));
47
+
48
+        // Hook for password change
49
+        $this->add_hook('password_ldap_bind', array($this, 'password_ldap_bind'));
50
+
51
+        // Hooks related to "Login As" feature
52
+        $this->add_hook('template_object_loginform', array($this, 'login_form'));
53
+        $this->add_hook('storage_connect', array($this, 'imap_connect'));
54
+        $this->add_hook('managesieve_connect', array($this, 'imap_connect'));
55
+        $this->add_hook('smtp_connect', array($this, 'smtp_connect'));
56
+        $this->add_hook('identity_form', array($this, 'identity_form'));
57
+
58
+        // Hook to modify some configuration, e.g. ldap
59
+        $this->add_hook('config_get', array($this, 'config_get'));
60
+
61
+        // Hook to modify logging directory
62
+        $this->add_hook('write_log', array($this, 'write_log'));
63
+        $this->username = $_SESSION['username'];
64
+
65
+        // Enable debug logs (per-user), when logged as another user
66
+        if (!empty($_SESSION['kolab_auth_admin']) && $rcmail->config->get('kolab_auth_auditlog')) {
67
+            $rcmail->config->set('debug_level', 1);
68
+            $rcmail->config->set('devel_mode', true);
69
+            $rcmail->config->set('smtp_log', true);
70
+            $rcmail->config->set('log_logins', true);
71
+            $rcmail->config->set('log_session', true);
72
+            $rcmail->config->set('memcache_debug', true);
73
+            $rcmail->config->set('imap_debug', true);
74
+            $rcmail->config->set('ldap_debug', true);
75
+            $rcmail->config->set('smtp_debug', true);
76
+            $rcmail->config->set('sql_debug', true);
77
+
78
+            // SQL debug need to be set directly on DB object
79
+            // setting config variable will not work here because
80
+            // the object is already initialized/configured
81
+            if ($db = $rcmail->get_dbh()) {
82
+                $db->set_debug(true);
83
+            }
84
+        }
85
+    }
86
+
87
+    /**
88
+     * Startup hook handler
89
+     */
90
+    public function startup($args)
91
+    {
92
+        // Check access rights when logged in as another user
93
+        if (!empty($_SESSION['kolab_auth_admin']) && $args['task'] != 'login' && $args['task'] != 'logout') {
94
+            // access to specified task is forbidden,
95
+            // redirect to the first task on the list
96
+            if (!empty($_SESSION['kolab_auth_allowed_tasks'])) {
97
+                $tasks = (array)$_SESSION['kolab_auth_allowed_tasks'];
98
+                if (!in_array($args['task'], $tasks) && !in_array('*', $tasks)) {
99
+                    header('Location: ?_task=' . array_shift($tasks));
100
+                    die;
101
+                }
102
+
103
+                // add script that will remove disabled taskbar buttons
104
+                if (!in_array('*', $tasks)) {
105
+                    $this->add_hook('render_page', array($this, 'render_page'));
106
+                }
107
+            }
108
+        }
109
+
110
+        // load per-user settings
111
+        $this->load_user_role_plugins_and_settings();
112
+
113
+        return $args;
114
+    }
115
+
116
+    /**
117
+     * Modify some configuration according to LDAP user record
118
+     */
119
+    public function config_get($args)
120
+    {
121
+        // Replaces ldap_vars (%dc, etc) in public kolab ldap addressbooks
122
+        // config based on the users base_dn. (for multi domain support)
123
+        if ($args['name'] == 'ldap_public' && !empty($args['result'])) {
124
+            $rcmail      = rcube::get_instance();
125
+            $kolab_books = (array) $rcmail->config->get('kolab_auth_ldap_addressbooks');
126
+
127
+            foreach ($args['result'] as $name => $config) {
128
+                if (in_array($name, $kolab_books) || in_array('*', $kolab_books)) {
129
+                    $args['result'][$name] = $this->patch_ldap_config($config);
130
+                }
131
+            }
132
+        }
133
+        else if ($args['name'] == 'kolab_users_directory' && !empty($args['result'])) {
134
+            $args['result'] = $this->patch_ldap_config($args['result']);
135
+        }
136
+
137
+        return $args;
138
+    }
139
+
140
+    /**
141
+     * Helper method to patch the given LDAP directory config with user-specific values
142
+     */
143
+    protected function patch_ldap_config($config)
144
+    {
145
+        if (is_array($config)) {
146
+            $config['base_dn']        = self::parse_ldap_vars($config['base_dn']);
147
+            $config['search_base_dn'] = self::parse_ldap_vars($config['search_base_dn']);
148
+            $config['bind_dn']        = str_replace('%dn', $_SESSION['kolab_dn'], $config['bind_dn']);
149
+
150
+            if (!empty($config['groups'])) {
151
+                $config['groups']['base_dn'] = self::parse_ldap_vars($config['groups']['base_dn']);
152
+            }
153
+        }
154
+
155
+        return $config;
156
+    }
157
+
158
+    /**
159
+     * Modifies list of plugins and settings according to
160
+     * specified LDAP roles
161
+     */
162
+    public function load_user_role_plugins_and_settings()
163
+    {
164
+        if (empty($_SESSION['user_roledns'])) {
165
+            return;
166
+        }
167
+
168
+        $rcmail = rcube::get_instance();
169
+
170
+        // Example 'kolab_auth_role_plugins' =
171
+        //
172
+        //  Array(
173
+        //      '<role_dn>' => Array('plugin1', 'plugin2'),
174
+        //  );
175
+        //
176
+        // NOTE that <role_dn> may in fact be something like: 'cn=role,%dc'
177
+
178
+        $role_plugins = $rcmail->config->get('kolab_auth_role_plugins');
179
+
180
+        // Example $rcmail_config['kolab_auth_role_settings'] =
181
+        //
182
+        //  Array(
183
+        //      '<role_dn>' => Array(
184
+        //          '$setting' => Array(
185
+        //              'mode' => '(override|merge)', (default: override)
186
+        //              'value' => <>,
187
+        //              'allow_override' => (true|false) (default: false)
188
+        //          ),
189
+        //      ),
190
+        //  );
191
+        //
192
+        // NOTE that <role_dn> may in fact be something like: 'cn=role,%dc'
193
+
194
+        $role_settings = $rcmail->config->get('kolab_auth_role_settings');
195
+
196
+        if (!empty($role_plugins)) {
197
+            foreach ($role_plugins as $role_dn => $plugins) {
198
+                $role_dn = self::parse_ldap_vars($role_dn);
199
+                if (!empty($role_plugins[$role_dn])) {
200
+                    $role_plugins[$role_dn] = array_unique(array_merge((array)$role_plugins[$role_dn], $plugins));
201
+                } else {
202
+                    $role_plugins[$role_dn] = $plugins;
203
+                }
204
+            }
205
+        }
206
+
207
+        if (!empty($role_settings)) {
208
+            foreach ($role_settings as $role_dn => $settings) {
209
+                $role_dn = self::parse_ldap_vars($role_dn);
210
+                if (!empty($role_settings[$role_dn])) {
211
+                    $role_settings[$role_dn] = array_merge((array)$role_settings[$role_dn], $settings);
212
+                } else {
213
+                    $role_settings[$role_dn] = $settings;
214
+                }
215
+            }
216
+        }
217
+
218
+        foreach ($_SESSION['user_roledns'] as $role_dn) {
219
+            if (!empty($role_settings[$role_dn]) && is_array($role_settings[$role_dn])) {
220
+                foreach ($role_settings[$role_dn] as $setting_name => $setting) {
221
+                    if (!isset($setting['mode'])) {
222
+                        $setting['mode'] = 'override';
223
+                    }
224
+
225
+                    if ($setting['mode'] == "override") {
226
+                        $rcmail->config->set($setting_name, $setting['value']);
227
+                    } elseif ($setting['mode'] == "merge") {
228
+                        $orig_setting = $rcmail->config->get($setting_name);
229
+
230
+                        if (!empty($orig_setting)) {
231
+                            if (is_array($orig_setting)) {
232
+                                $rcmail->config->set($setting_name, array_merge($orig_setting, $setting['value']));
233
+                            }
234
+                        } else {
235
+                            $rcmail->config->set($setting_name, $setting['value']);
236
+                        }
237
+                    }
238
+
239
+                    $dont_override = (array) $rcmail->config->get('dont_override');
240
+
241
+                    if (empty($setting['allow_override'])) {
242
+                        $rcmail->config->set('dont_override', array_merge($dont_override, array($setting_name)));
243
+                    }
244
+                    else {
245
+                        if (in_array($setting_name, $dont_override)) {
246
+                            $_dont_override = array();
247
+                            foreach ($dont_override as $_setting) {
248
+                                if ($_setting != $setting_name) {
249
+                                    $_dont_override[] = $_setting;
250
+                                }
251
+                            }
252
+                            $rcmail->config->set('dont_override', $_dont_override);
253
+                        }
254
+                    }
255
+
256
+                    if ($setting_name == 'skin') {
257
+                        if ($rcmail->output->type == 'html') {
258
+                            $rcmail->output->set_skin($setting['value']);
259
+                            $rcmail->output->set_env('skin', $setting['value']);
260
+                        }
261
+                    }
262
+                }
263
+            }
264
+
265
+            if (!empty($role_plugins[$role_dn])) {
266
+                foreach ((array)$role_plugins[$role_dn] as $plugin) {
267
+                    $this->api->load_plugin($plugin);
268
+                }
269
+            }
270
+        }
271
+    }
272
+
273
+    /**
274
+     * Logging method replacement to print debug/errors into
275
+     * a separate (sub)folder for each user
276
+     */
277
+    public function write_log($args)
278
+    {
279
+        $rcmail = rcube::get_instance();
280
+
281
+        if ($rcmail->config->get('log_driver') == 'syslog') {
282
+            return $args;
283
+        }
284
+
285
+        // log_driver == 'file' is assumed here
286
+        $log_dir  = $rcmail->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
287
+
288
+        // Append original username + target username for audit-logging
289
+        if ($rcmail->config->get('kolab_auth_auditlog') && !empty($_SESSION['kolab_auth_admin'])) {
290
+            $args['dir'] = $log_dir . '/' . strtolower($_SESSION['kolab_auth_admin']) . '/' . strtolower($this->username);
291
+
292
+            // Attempt to create the directory
293
+            if (!is_dir($args['dir'])) {
294
+                @mkdir($args['dir'], 0750, true);
295
+            }
296
+        }
297
+        // Define the user log directory if a username is provided
298
+        else if ($rcmail->config->get('per_user_logging') && !empty($this->username)) {
299
+            $user_log_dir = $log_dir . '/' . strtolower($this->username);
300
+            if (is_writable($user_log_dir)) {
301
+                $args['dir'] = $user_log_dir;
302
+            }
303
+            else if ($args['name'] != 'errors') {
304
+                $args['abort'] = true;  // don't log if unauthenticed
305
+            }
306
+        }
307
+
308
+        return $args;
309
+    }
310
+
311
+    /**
312
+     * Sets defaults for new user.
313
+     */
314
+    public function user_create($args)
315
+    {
316
+        if (!empty($this->data['user_email'])) {
317
+            // addresses list is supported
318
+            if (array_key_exists('email_list', $args)) {
319
+                $email_list = array_unique($this->data['user_email']);
320
+
321
+                // add organization to the list
322
+                if (!empty($this->data['user_organization'])) {
323
+                    foreach ($email_list as $idx => $email) {
324
+                        $email_list[$idx] = array(
325
+                            'organization' => $this->data['user_organization'],
326
+                            'email'        => $email,
327
+                        );
328
+                    }
329
+                }
330
+
331
+                $args['email_list'] = $email_list;
332
+            }
333
+            else {
334
+                $args['user_email'] = $this->data['user_email'][0];
335
+            }
336
+        }
337
+
338
+        if (!empty($this->data['user_name'])) {
339
+            $args['user_name'] = $this->data['user_name'];
340
+        }
341
+
342
+        return $args;
343
+    }
344
+
345
+    /**
346
+     * Modifies login form adding additional "Login As" field
347
+     */
348
+    public function login_form($args)
349
+    {
350
+        $this->add_texts('localization/');
351
+
352
+        $rcmail      = rcube::get_instance();
353
+        $admin_login = $rcmail->config->get('kolab_auth_admin_login');
354
+        $group       = $rcmail->config->get('kolab_auth_group');
355
+        $role_attr   = $rcmail->config->get('kolab_auth_role');
356
+
357
+        // Show "Login As" input
358
+        if (empty($admin_login) || (empty($group) && empty($role_attr))) {
359
+            return $args;
360
+        }
361
+
362
+        $input = new html_inputfield(array('name' => '_loginas', 'id' => 'rcmloginas',
363
+            'type' => 'text', 'autocomplete' => 'off'));
364
+        $row = html::tag('tr', null,
365
+            html::tag('td', 'title', html::label('rcmloginas', Q($this->gettext('loginas'))))
366
+            . html::tag('td', 'input', $input->show(trim(rcube_utils::get_input_value('_loginas', rcube_utils::INPUT_POST))))
367
+        );
368
+        $args['content'] = preg_replace('/<\/tbody>/i', $row . '</tbody>', $args['content']);
369
+
370
+        return $args;
371
+    }
372
+
373
+    /**
374
+     * Find user credentials In LDAP.
375
+     */
376
+    public function authenticate($args)
377
+    {
378
+        // get username and host
379
+        $host    = $args['host'];
380
+        $user    = $args['user'];
381
+        $pass    = $args['pass'];
382
+        $loginas = trim(rcube_utils::get_input_value('_loginas', rcube_utils::INPUT_POST));
383
+
384
+        if (empty($user) || empty($pass)) {
385
+            $args['abort'] = true;
386
+            return $args;
387
+        }
388
+
389
+        // temporarily set the current username to the one submitted
390
+        $this->username = $user;
391
+
392
+        $ldap = self::ldap();
393
+        if (!$ldap || !$ldap->ready) {
394
+            $args['abort'] = true;
395
+            $args['kolab_ldap_error'] = true;
396
+            $message = sprintf(
397
+                    'Login failure for user %s from %s in session %s (error %s)',
398
+                    $user,
399
+                    rcube_utils::remote_ip(),
400
+                    session_id(),
401
+                    "LDAP not ready"
402
+                );
403
+
404
+            rcube::write_log('userlogins', $message);
405
+
406
+            return $args;
407
+        }
408
+
409
+        // Find user record in LDAP
410
+        $record = $ldap->get_user_record($user, $host);
411
+
412
+        if (empty($record)) {
413
+            $args['abort'] = true;
414
+            $message = sprintf(
415
+                    'Login failure for user %s from %s in session %s (error %s)',
416
+                    $user,
417
+                    rcube_utils::remote_ip(),
418
+                    session_id(),
419
+                    "No user record found"
420
+                );
421
+
422
+            rcube::write_log('userlogins', $message);
423
+
424
+            return $args;
425
+        }
426
+
427
+        $rcmail      = rcube::get_instance();
428
+        $admin_login = $rcmail->config->get('kolab_auth_admin_login');
429
+        $admin_pass  = $rcmail->config->get('kolab_auth_admin_password');
430
+        $login_attr  = $rcmail->config->get('kolab_auth_login');
431
+        $name_attr   = $rcmail->config->get('kolab_auth_name');
432
+        $email_attr  = $rcmail->config->get('kolab_auth_email');
433
+        $org_attr    = $rcmail->config->get('kolab_auth_organization');
434
+        $role_attr   = $rcmail->config->get('kolab_auth_role');
435
+        $imap_attr   = $rcmail->config->get('kolab_auth_mailhost');
436
+
437
+        if (!empty($role_attr) && !empty($record[$role_attr])) {
438
+            $_SESSION['user_roledns'] = (array)($record[$role_attr]);
439
+        }
440
+
441
+        if (!empty($imap_attr) && !empty($record[$imap_attr])) {
442
+            $default_host = $rcmail->config->get('default_host');
443
+            if (!empty($default_host)) {
444
+                rcube::write_log("errors", "Both default host and kolab_auth_mailhost set. Incompatible.");
445
+            } else {
446
+                $args['host'] = "tls://" . $record[$imap_attr];
447
+            }
448
+        }
449
+
450
+        // Login As...
451
+        if (!empty($loginas) && $admin_login) {
452
+            // Authenticate to LDAP
453
+            $result = $ldap->bind($record['dn'], $pass);
454
+
455
+            if (!$result) {
456
+                $args['abort'] = true;
457
+                $message = sprintf(
458
+                        'Login failure for user %s from %s in session %s (error %s)',
459
+                        $user,
460
+                        rcube_utils::remote_ip(),
461
+                        session_id(),
462
+                        "Unable to bind with '" . $record['dn'] . "'"
463
+                    );
464
+
465
+                rcube::write_log('userlogins', $message);
466
+
467
+                return $args;
468
+            }
469
+
470
+            $isadmin = false;
471
+            $admin_rights = $rcmail->config->get('kolab_auth_admin_rights', array());
472
+
473
+            // @deprecated: fall-back to the old check if the original user has/belongs to administrative role/group
474
+            if (empty($admin_rights)) {
475
+                $group   = $rcmail->config->get('kolab_auth_group');
476
+                $role_dn = $rcmail->config->get('kolab_auth_role_value');
477
+
478
+                // check role attribute
479
+                if (!empty($role_attr) && !empty($role_dn) && !empty($record[$role_attr])) {
480
+                    $role_dn = $ldap->parse_vars($role_dn, $user, $host);
481
+                    if (in_array($role_dn, (array)$record[$role_attr])) {
482
+                        $isadmin = true;
483
+                    }
484
+                }
485
+
486
+                // check group
487
+                if (!$isadmin && !empty($group)) {
488
+                    $groups = $ldap->get_user_groups($record['dn'], $user, $host);
489
+                    if (in_array($group, $groups)) {
490
+                        $isadmin = true;
491
+                    }
492
+                }
493
+
494
+                if ($isadmin) {
495
+                    // user has admin privileges privilage, get "login as" user credentials
496
+                    $target_entry = $ldap->get_user_record($loginas, $host);
497
+                    $allowed_tasks = $rcmail->config->get('kolab_auth_allowed_tasks');
498
+                }
499
+            }
500
+            else {
501
+                // get "login as" user credentials
502
+                $target_entry = $ldap->get_user_record($loginas, $host);
503
+
504
+                if (!empty($target_entry)) {
505
+                    // get effective rights to determine login-as permissions
506
+                    $effective_rights = (array)$ldap->effective_rights($target_entry['dn']);
507
+
508
+                    if (!empty($effective_rights)) {
509
+                        $effective_rights['attrib'] = $effective_rights['attributeLevelRights'];
510
+                        $effective_rights['entry']  = $effective_rights['entryLevelRights'];
511
+
512
+                        // compare the rights with the permissions mapping
513
+                        $allowed_tasks = array();
514
+                        foreach ($admin_rights as $task => $perms) {
515
+                            $perms_ = explode(':', $perms);
516
+                            $type   = array_shift($perms_);
517
+                            $req    = array_pop($perms_);
518
+                            $attrib = array_pop($perms_);
519
+
520
+                            if (array_key_exists($type, $effective_rights)) {
521
+                                if ($type == 'entry' && in_array($req, $effective_rights[$type])) {
522
+                                    $allowed_tasks[] = $task;
523
+                                }
524
+                                else if ($type == 'attrib' && array_key_exists($attrib, $effective_rights[$type]) &&
525
+                                        in_array($req, $effective_rights[$type][$attrib])) {
526
+                                    $allowed_tasks[] = $task;
527
+                                }
528
+                            }
529
+                        }
530
+
531
+                        $isadmin = !empty($allowed_tasks);
532
+                    }
533
+                }
534
+            }
535
+
536
+            // Save original user login for log (see below)
537
+            if ($login_attr) {
538
+                $origname = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
539
+            }
540
+            else {
541
+                $origname = $user;
542
+            }
543
+
544
+            if (!$isadmin || empty($target_entry)) {
545
+                $this->add_texts('localization/');
546
+
547
+                $args['abort'] = true;
548
+                $args['error'] = $this->gettext(array(
549
+                    'name' => 'loginasnotallowed',
550
+                    'vars' => array('user' => Q($loginas)),
551
+                ));
552
+
553
+                $message = sprintf(
554
+                        'Login failure for user %s (as user %s) from %s in session %s (error %s)',
555
+                        $user,
556
+                        $loginas,
557
+                        rcube_utils::remote_ip(),
558
+                        session_id(),
559
+                        "No privileges to login as '" . $loginas . "'"
560
+                    );
561
+
562
+                rcube::write_log('userlogins', $message);
563
+
564
+                return $args;
565
+            }
566
+
567
+            // replace $record with target entry
568
+            $record = $target_entry;
569
+
570
+            $args['user'] = $this->username = $loginas;
571
+
572
+            // Mark session to use SASL proxy for IMAP authentication
573
+            $_SESSION['kolab_auth_admin']    = strtolower($origname);
574
+            $_SESSION['kolab_auth_login']    = $rcmail->encrypt($admin_login);
575
+            $_SESSION['kolab_auth_password'] = $rcmail->encrypt($admin_pass);
576
+            $_SESSION['kolab_auth_allowed_tasks'] = $allowed_tasks;
577
+        }
578
+
579
+        // Store UID and DN of logged user in session for use by other plugins
580
+        $_SESSION['kolab_uid'] = is_array($record['uid']) ? $record['uid'][0] : $record['uid'];
581
+        $_SESSION['kolab_dn']  = $record['dn'];
582
+
583
+        // Store LDAP replacement variables used for current user
584
+        // This improves performance of load_user_role_plugins_and_settings()
585
+        // which is executed on every request (via startup hook) and where
586
+        // we don't like to use LDAP (connection + bind + search)
587
+        $_SESSION['kolab_auth_vars'] = $ldap->get_parse_vars();
588
+
589
+        // Set user login
590
+        if ($login_attr) {
591
+            $this->data['user_login'] = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
592
+        }
593
+        if ($this->data['user_login']) {
594
+            $args['user'] = $this->username = $this->data['user_login'];
595
+        }
596
+
597
+        // User name for identity (first log in)
598
+        foreach ((array)$name_attr as $field) {
599
+            $name = is_array($record[$field]) ? $record[$field][0] : $record[$field];
600
+            if (!empty($name)) {
601
+                $this->data['user_name'] = $name;
602
+                break;
603
+            }
604
+        }
605
+        // User email(s) for identity (first log in)
606
+        foreach ((array)$email_attr as $field) {
607
+            $email = is_array($record[$field]) ? array_filter($record[$field]) : $record[$field];
608
+            if (!empty($email)) {
609
+                $this->data['user_email'] = array_merge((array)$this->data['user_email'], (array)$email);
610
+            }
611
+        }
612
+        // Organization name for identity (first log in)
613
+        foreach ((array)$org_attr as $field) {
614
+            $organization = is_array($record[$field]) ? $record[$field][0] : $record[$field];
615
+            if (!empty($organization)) {
616
+                $this->data['user_organization'] = $organization;
617
+                break;
618
+            }
619
+        }
620
+
621
+        // Log "Login As" usage
622
+        if (!empty($origname)) {
623
+            rcube::write_log('userlogins', sprintf('Admin login for %s by %s from %s',
624
+                $args['user'], $origname, rcube_utils::remote_ip()));
625
+        }
626
+
627
+        // load per-user settings/plugins
628
+        $this->load_user_role_plugins_and_settings();
629
+
630
+        return $args;
631
+    }
632
+
633
+    /**
634
+     * Set user DN for password change (password plugin with ldap_simple driver)
635
+     */
636
+    public function password_ldap_bind($args)
637
+    {
638
+        $args['user_dn'] = $_SESSION['kolab_dn'];
639
+
640
+        $rcmail = rcube::get_instance();
641
+
642
+        $rcmail->config->set('password_ldap_method', 'user');
643
+
644
+        return $args;
645
+    }
646
+
647
+    /**
648
+     * Sets SASL Proxy login/password for IMAP and Managesieve auth
649
+     */
650
+    public function imap_connect($args)
651
+    {
652
+        if (!empty($_SESSION['kolab_auth_admin'])) {
653
+            $rcmail      = rcube::get_instance();
654
+            $admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
655
+            $admin_pass  = $rcmail->decrypt($_SESSION['kolab_auth_password']);
656
+
657
+            $args['auth_cid'] = $admin_login;
658
+            $args['auth_pw']  = $admin_pass;
659
+        }
660
+
661
+        return $args;
662
+    }
663
+
664
+    /**
665
+     * Sets SASL Proxy login/password for SMTP auth
666
+     */
667
+    public function smtp_connect($args)
668
+    {
669
+        if (!empty($_SESSION['kolab_auth_admin'])) {
670
+            $rcmail      = rcube::get_instance();
671
+            $admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
672
+            $admin_pass  = $rcmail->decrypt($_SESSION['kolab_auth_password']);
673
+
674
+            $args['smtp_auth_cid'] = $admin_login;
675
+            $args['smtp_auth_pw']  = $admin_pass;
676
+        }
677
+
678
+        return $args;
679
+    }
680
+
681
+    /**
682
+     * Hook to replace the plain text input field for email address by a drop-down list
683
+     * with all email addresses (including aliases) from this user's LDAP record.
684
+     */
685
+    public function identity_form($args)
686
+    {
687
+        $rcmail      = rcube::get_instance();
688
+        $ident_level = intval($rcmail->config->get('identities_level', 0));
689
+
690
+        // do nothing if email address modification is disabled
691
+        if ($ident_level == 1 || $ident_level == 3) {
692
+            return $args;
693
+        }
694
+
695
+        $ldap = self::ldap();
696
+        if (!$ldap || !$ldap->ready || empty($_SESSION['kolab_dn'])) {
697
+            return $args;
698
+        }
699
+
700
+        $emails      = array();
701
+        $user_record = $ldap->get_record($_SESSION['kolab_dn']);
702
+
703
+        foreach ((array)$rcmail->config->get('kolab_auth_email', array()) as $col) {
704
+            $values = rcube_addressbook::get_col_values($col, $user_record, true);
705
+            if (!empty($values))
706
+                $emails = array_merge($emails, array_filter($values));
707
+        }
708
+
709
+        // kolab_delegation might want to modify this addresses list
710
+        $plugin = $rcmail->plugins->exec_hook('kolab_auth_emails', array('emails' => $emails));
711
+        $emails = $plugin['emails'];
712
+
713
+        if (!empty($emails)) {
714
+            $args['form']['addressing']['content']['email'] = array(
715
+                'type' => 'select',
716
+                'options' => array_combine($emails, $emails),
717
+            );
718
+        }
719
+
720
+        return $args;
721
+    }
722
+
723
+    /**
724
+     * Action executed before the page is rendered to add an onload script
725
+     * that will remove all taskbar buttons for disabled tasks
726
+     */
727
+    public function render_page($args)
728
+    {
729
+        $rcmail  = rcube::get_instance();
730
+        $tasks   = (array)$_SESSION['kolab_auth_allowed_tasks'];
731
+        $tasks[] = 'logout';
732
+
733
+        // disable buttons in taskbar
734
+        $script = "
735
+        \$('a').filter(function() {
736
+            var ev = \$(this).attr('onclick');
737
+            return ev && ev.match(/'switch-task','([a-z]+)'/)
738
+                && \$.inArray(RegExp.\$1, " . json_encode($tasks) . ") < 0;
739
+        }).remove();
740
+        ";
741
+
742
+        $rcmail->output->add_script($script, 'docready');
743
+    }
744
+
745
+    /**
746
+     * Initializes LDAP object and connects to LDAP server
747
+     */
748
+    public static function ldap()
749
+    {
750
+        if (self::$ldap) {
751
+            return self::$ldap;
752
+        }
753
+
754
+        $rcmail      = rcube::get_instance();
755
+        $addressbook = $rcmail->config->get('kolab_auth_addressbook');
756
+
757
+        if (!is_array($addressbook)) {
758
+            $ldap_config = (array)$rcmail->config->get('ldap_public');
759
+            $addressbook = $ldap_config[$addressbook];
760
+        }
761
+
762
+        if (empty($addressbook)) {
763
+            return null;
764
+        }
765
+
766
+        require_once __DIR__ . '/kolab_auth_ldap.php';
767
+
768
+        self::$ldap = new kolab_auth_ldap($addressbook);
769
+
770
+        return self::$ldap;
771
+    }
772
+
773
+    /**
774
+     * Parses LDAP DN string with replacing supported variables.
775
+     * See kolab_auth_ldap::parse_vars()
776
+     *
777
+     * @param string $str LDAP DN string
778
+     *
779
+     * @return string Parsed DN string
780
+     */
781
+    public static function parse_ldap_vars($str)
782
+    {
783
+        if (!empty($_SESSION['kolab_auth_vars'])) {
784
+            $str = strtr($str, $_SESSION['kolab_auth_vars']);
785
+        }
786
+
787
+        return $str;
788
+    }
789
+}
790
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/kolab_auth_ldap.php Added
550
 
1
@@ -0,0 +1,548 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab Authentication
6
+ *
7
+ * @version @package_version@
8
+ * @author Aleksander Machniak <machniak@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2011-2013, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+
26
+/**
27
+ * Wrapper class for rcube_ldap_generic
28
+ */
29
+class kolab_auth_ldap extends rcube_ldap_generic
30
+{
31
+    private $icache = array();
32
+    private $conf = array();
33
+    private $fieldmap = array();
34
+
35
+
36
+    function __construct($p)
37
+    {
38
+        $rcmail = rcube::get_instance();
39
+
40
+        $this->conf = $p;
41
+        $this->conf['kolab_auth_user_displayname'] = $rcmail->config->get('kolab_auth_user_displayname', '{name}');
42
+
43
+        $this->fieldmap = $p['fieldmap'];
44
+        $this->fieldmap['uid'] = 'uid';
45
+
46
+        $p['attributes'] = array_values($this->fieldmap);
47
+        $p['debug']      = (bool) $rcmail->config->get('ldap_debug');
48
+
49
+        // Connect to the server (with bind)
50
+        parent::__construct($p);
51
+        $this->_connect();
52
+
53
+        $rcmail->add_shutdown_function(array($this, 'close'));
54
+    }
55
+
56
+    /**
57
+    * Establish a connection to the LDAP server
58
+    */
59
+    private function _connect()
60
+    {
61
+        // try to connect + bind for every host configured
62
+        // with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
63
+        // see http://www.php.net/manual/en/function.ldap-connect.php
64
+        foreach ((array)$this->config['hosts'] as $host) {
65
+            // skip host if connection failed
66
+            if (!$this->connect($host)) {
67
+                continue;
68
+            }
69
+
70
+            $bind_pass = $this->config['bind_pass'];
71
+            $bind_user = $this->config['bind_user'];
72
+            $bind_dn   = $this->config['bind_dn'];
73
+
74
+            if (empty($bind_pass)) {
75
+                $this->ready = true;
76
+            }
77
+            else {
78
+                if (!empty($bind_dn)) {
79
+                    $this->ready = $this->bind($bind_dn, $bind_pass);
80
+                }
81
+                else if (!empty($this->config['auth_cid'])) {
82
+                    $this->ready = $this->sasl_bind($this->config['auth_cid'], $bind_pass, $bind_user);
83
+                }
84
+                else {
85
+                    $this->ready = $this->sasl_bind($bind_user, $bind_pass);
86
+                }
87
+            }
88
+
89
+            // connection established, we're done here
90
+            if ($this->ready) {
91
+                break;
92
+            }
93
+
94
+        }  // end foreach hosts
95
+
96
+        if (!is_resource($this->conn)) {
97
+            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
98
+                'file' => __FILE__, 'line' => __LINE__,
99
+                'message' => "Could not connect to any LDAP server, last tried $host"), true);
100
+
101
+            $this->ready = false;
102
+        }
103
+
104
+        return $this->ready;
105
+    }
106
+
107
+    /**
108
+     * Fetches user data from LDAP addressbook
109
+     */
110
+    function get_user_record($user, $host)
111
+    {
112
+        $rcmail  = rcube::get_instance();
113
+        $filter  = $rcmail->config->get('kolab_auth_filter');
114
+        $filter  = $this->parse_vars($filter, $user, $host);
115
+        $base_dn = $this->parse_vars($this->config['base_dn'], $user, $host);
116
+        $scope   = $this->config['scope'];
117
+
118
+        // @TODO: print error if filter is empty
119
+
120
+        // get record
121
+        if ($result = parent::search($base_dn, $filter, $scope, $this->attributes)) {
122
+            if ($result->count() == 1) {
123
+                $entries = $result->entries(true);
124
+                $dn      = key($entries);
125
+                $entry   = array_pop($entries);
126
+                $entry   = $this->field_mapping($dn, $entry);
127
+
128
+                return $entry;
129
+            }
130
+        }
131
+    }
132
+
133
+    /**
134
+     * Fetches user data from LDAP addressbook
135
+     */
136
+    function get_user_groups($dn, $user, $host)
137
+    {
138
+        if (empty($dn) || empty($this->config['groups'])) {
139
+            return array();
140
+        }
141
+
142
+        $base_dn     = $this->parse_vars($this->config['groups']['base_dn'], $user, $host);
143
+        $name_attr   = $this->config['groups']['name_attr'] ? $this->config['groups']['name_attr'] : 'cn';
144
+        $member_attr = $this->get_group_member_attr();
145
+        $filter      = "(member=$dn)(uniqueMember=$dn)";
146
+
147
+        if ($member_attr != 'member' && $member_attr != 'uniqueMember')
148
+            $filter .= "($member_attr=$dn)";
149
+        $filter = strtr("(|$filter)", array("\\" => "\\\\"));
150
+
151
+        $result = parent::search($base_dn, $filter, 'sub', array('dn', $name_attr));
152
+
153
+        if (!$result) {
154
+            return array();
155
+        }
156
+
157
+        $groups = array();
158
+        foreach ($result as $entry) {
159
+            $dn    = $entry['dn'];
160
+            $entry = rcube_ldap_generic::normalize_entry($entry);
161
+
162
+            $groups[$dn] = $entry[$name_attr];
163
+        }
164
+
165
+        return $groups;
166
+    }
167
+
168
+    /**
169
+     * Get a specific LDAP record
170
+     *
171
+     * @param string DN
172
+     *
173
+     * @return array Record data
174
+     */
175
+    function get_record($dn)
176
+    {
177
+        if (!$this->ready) {
178
+            return;
179
+        }
180
+
181
+        if ($rec = $this->get_entry($dn)) {
182
+            $rec = rcube_ldap_generic::normalize_entry($rec);
183
+            $rec = $this->field_mapping($dn, $rec);
184
+        }
185
+
186
+        return $rec;
187
+    }
188
+
189
+    /**
190
+     * Replace LDAP record data items
191
+     *
192
+     * @param string $dn    DN
193
+     * @param array  $entry LDAP entry
194
+     *
195
+     * return bool True on success, False on failure
196
+     */
197
+    function replace($dn, $entry)
198
+    {
199
+        // fields mapping
200
+        foreach ($this->fieldmap as $field => $attr) {
201
+            if (array_key_exists($field, $entry)) {
202
+                $entry[$attr] = $entry[$field];
203
+                if ($attr != $field) {
204
+                    unset($entry[$field]);
205
+                }
206
+            }
207
+        }
208
+
209
+        return $this->mod_replace($dn, $entry);
210
+    }
211
+
212
+    /**
213
+     * Search records (simplified version of rcube_ldap::search)
214
+     *
215
+     * @param mixed   $fields   The field name or array of field names to search in
216
+     * @param mixed   $value    Search value (or array of values when $fields is array)
217
+     * @param int     $mode     Matching mode:
218
+     *                          0 - partial (*abc*),
219
+     *                          1 - strict (=),
220
+     *                          2 - prefix (abc*)
221
+     * @param array   $required List of fields that cannot be empty
222
+     * @param int     $limit    Number of records
223
+     * @param int     $count    Returns the number of records found
224
+     *
225
+     * @return array List or false on error
226
+     */
227
+    function dosearch($fields, $value, $mode=1, $required = array(), $limit = 0, &$count = 0)
228
+    {
229
+        if (empty($fields)) {
230
+            return array();
231
+        }
232
+
233
+        $mode = intval($mode);
234
+
235
+        // use AND operator for advanced searches
236
+        $filter = is_array($value) ? '(&' : '(|';
237
+
238
+        // set wildcards
239
+        $wp = $ws = '';
240
+        if (!empty($this->config['fuzzy_search']) && $mode != 1) {
241
+            $ws = '*';
242
+            if (!$mode) {
243
+                $wp = '*';
244
+            }
245
+        }
246
+
247
+        foreach ((array)$fields as $idx => $field) {
248
+            $val   = is_array($value) ? $value[$idx] : $value;
249
+            $attrs = (array) $this->fieldmap[$field];
250
+
251
+            if (empty($attrs)) {
252
+                $filter .= "($field=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
253
+            }
254
+            else {
255
+                if (count($attrs) > 1)
256
+                    $filter .= '(|';
257
+                foreach ($attrs as $f)
258
+                    $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
259
+                if (count($attrs) > 1)
260
+                    $filter .= ')';
261
+            }
262
+        }
263
+        $filter .= ')';
264
+
265
+        // add required (non empty) fields filter
266
+        $req_filter = '';
267
+
268
+        foreach ((array)$required as $field) {
269
+            if (in_array($field, (array)$fields))  // required field is already in search filter
270
+                continue;
271
+
272
+            $attrs = (array) $this->fieldmap[$field];
273
+
274
+            if (empty($attrs)) {
275
+                $req_filter .= "($field=*)";
276
+            }
277
+            else {
278
+                if (count($attrs) > 1)
279
+                    $req_filter .= '(|';
280
+                foreach ($attrs as $f)
281
+                    $req_filter .= "($f=*)";
282
+                if (count($attrs) > 1)
283
+                    $req_filter .= ')';
284
+            }
285
+        }
286
+
287
+        if (!empty($req_filter)) {
288
+            $filter = '(&' . $req_filter . $filter . ')';
289
+        }
290
+
291
+        // avoid double-wildcard if $value is empty
292
+        $filter = preg_replace('/\*+/', '*', $filter);
293
+
294
+        // add general filter to query
295
+        if (!empty($this->config['filter'])) {
296
+            $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->config['filter']) . ')' . $filter . ')';
297
+        }
298
+
299
+        $base_dn = $this->parse_vars($this->config['base_dn']);
300
+        $scope   = $this->config['scope'];
301
+        $attrs   = array_values($this->fieldmap);
302
+        $list    = array();
303
+
304
+        if ($result = $this->search($base_dn, $filter, $scope, $attrs)) {
305
+            $count = $result->count();
306
+            $i = 0;
307
+            foreach ($result as $entry) {
308
+                if ($limit && $limit <= $i) {
309
+                    break;
310
+                }
311
+
312
+                $dn        = $entry['dn'];
313
+                $entry     = rcube_ldap_generic::normalize_entry($entry);
314
+                $list[$dn] = $this->field_mapping($dn, $entry);
315
+                $i++;
316
+            }
317
+        }
318
+
319
+        return $list;
320
+    }
321
+
322
+    /**
323
+     * Set filter used in search()
324
+     */
325
+    function set_filter($filter)
326
+    {
327
+        $this->config['filter'] = $filter;
328
+    }
329
+
330
+    /**
331
+     * Maps LDAP attributes to defined fields
332
+     */
333
+    protected function field_mapping($dn, $entry)
334
+    {
335
+        $entry['dn'] = $dn;
336
+
337
+        // fields mapping
338
+        foreach ($this->fieldmap as $field => $attr) {
339
+            // $entry might be indexed by lower-case attribute names
340
+            $attr_lc = strtolower($attr);
341
+            if (isset($entry[$attr_lc])) {
342
+                $entry[$field] = $entry[$attr_lc];
343
+            }
344
+            else if (isset($entry[$attr])) {
345
+                $entry[$field] = $entry[$attr];
346
+            }
347
+        }
348
+
349
+        // compose display name according to config
350
+        if (empty($this->fieldmap['displayname'])) {
351
+            $entry['displayname'] = rcube_addressbook::compose_search_name(
352
+                $entry,
353
+                $entry['email'],
354
+                $entry['name'],
355
+                $this->conf['kolab_auth_user_displayname']
356
+            );
357
+        }
358
+
359
+        return $entry;
360
+    }
361
+
362
+    /**
363
+     * Detects group member attribute name
364
+     */
365
+    private function get_group_member_attr($object_classes = array())
366
+    {
367
+        if (empty($object_classes)) {
368
+            $object_classes = $this->config['groups']['object_classes'];
369
+        }
370
+        if (!empty($object_classes)) {
371
+            foreach ((array)$object_classes as $oc) {
372
+                switch (strtolower($oc)) {
373
+                    case 'group':
374
+                    case 'groupofnames':
375
+                    case 'kolabgroupofnames':
376
+                        $member_attr = 'member';
377
+                        break;
378
+
379
+                    case 'groupofuniquenames':
380
+                    case 'kolabgroupofuniquenames':
381
+                        $member_attr = 'uniqueMember';
382
+                        break;
383
+                }
384
+            }
385
+        }
386
+
387
+        if (!empty($member_attr)) {
388
+            return $member_attr;
389
+        }
390
+
391
+        if (!empty($this->config['groups']['member_attr'])) {
392
+            return $this->config['groups']['member_attr'];
393
+        }
394
+
395
+        return 'member';
396
+    }
397
+
398
+    /**
399
+     * Prepares filter query for LDAP search
400
+     */
401
+    function parse_vars($str, $user = null, $host = null)
402
+    {
403
+        // When authenticating user $user is always set
404
+        // if not set it means we use this LDAP object for other
405
+        // purposes, e.g. kolab_delegation, then username with
406
+        // correct domain is in a session
407
+        if (!$user) {
408
+            $user = $_SESSION['username'];
409
+        }
410
+
411
+        if (isset($this->icache[$user])) {
412
+            list($user, $dc) = $this->icache[$user];
413
+        }
414
+        else {
415
+            $orig_user = $user;
416
+            $rcmail = rcube::get_instance();
417
+
418
+            // get default domain
419
+            if ($username_domain = $rcmail->config->get('username_domain')) {
420
+                if ($host && is_array($username_domain) && isset($username_domain[$host])) {
421
+                    $domain = rcube_utils::parse_host($username_domain[$host], $host);
422
+                }
423
+                else if (is_string($username_domain)) {
424
+                    $domain = rcube_utils::parse_host($username_domain, $host);
425
+                }
426
+            }
427
+
428
+            // realmed username (with domain)
429
+            if (strpos($user, '@')) {
430
+                list($usr, $dom) = explode('@', $user);
431
+
432
+                // unrealm domain, user login can contain a domain alias
433
+                if ($dom != $domain && ($dc = $this->find_domain($dom))) {
434
+                    // @FIXME: we should replace domain in $user, I suppose
435
+                }
436
+            }
437
+            else if ($domain) {
438
+                $user .= '@' . $domain;
439
+            }
440
+
441
+            $this->icache[$orig_user] = array($user, $dc);
442
+        }
443
+
444
+        // replace variables in filter
445
+        list($u, $d) = explode('@', $user);
446
+
447
+        // hierarchal domain string
448
+        if (empty($dc)) {
449
+            $dc = 'dc=' . strtr($d, array('.' => ',dc='));
450
+        }
451
+
452
+        $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u);
453
+
454
+        $this->parse_replaces = $replaces;
455
+
456
+        return strtr($str, $replaces);
457
+    }
458
+
459
+    /**
460
+     * Find root domain for specified domain
461
+     *
462
+     * @param string $domain Domain name
463
+     *
464
+     * @return string Domain DN string
465
+     */
466
+    function find_domain($domain)
467
+    {
468
+        if (empty($domain) || empty($this->config['domain_base_dn']) || empty($this->config['domain_filter'])) {
469
+            return null;
470
+        }
471
+
472
+        $base_dn   = $this->config['domain_base_dn'];
473
+        $filter    = $this->config['domain_filter'];
474
+        $name_attr = $this->config['domain_name_attribute'];
475
+
476
+        if (empty($name_attr)) {
477
+            $name_attr = 'associateddomain';
478
+        }
479
+
480
+        $filter = str_replace('%s', rcube_ldap_generic::quote_string($domain), $filter);
481
+        $result = parent::search($base_dn, $filter, 'sub', array($name_attr, 'inetdomainbasedn'));
482
+
483
+        if (!$result) {
484
+            return null;
485
+        }
486
+
487
+        $entries  = $result->entries(true);
488
+        $entry_dn = key($entries);
489
+        $entry    = $entries[$entry_dn];
490
+
491
+        if (is_array($entry)) {
492
+            if (!empty($entry['inetdomainbasedn'])) {
493
+                return $entry['inetdomainbasedn'];
494
+            }
495
+
496
+            $domain = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
497
+
498
+            return $domain ? 'dc=' . implode(',dc=', explode('.', $domain)) : null;
499
+        }
500
+    }
501
+
502
+    /**
503
+     * Returns variables used for replacement in (last) parse_vars() call
504
+     *
505
+     * @return array Variable-value hash array
506
+     */
507
+    public function get_parse_vars()
508
+    {
509
+        return $this->parse_replaces;
510
+    }
511
+
512
+    /**
513
+     * Register additional fields
514
+     */
515
+    public function extend_fieldmap($map)
516
+    {
517
+        foreach ((array)$map as $name => $attr) {
518
+            if (!in_array($attr, $this->attributes)) {
519
+                $this->attributes[]    = $attr;
520
+                $this->fieldmap[$name] = $attr;
521
+            }
522
+        }
523
+    }
524
+
525
+    /**
526
+     * HTML-safe DN string encoding
527
+     *
528
+     * @param string $str DN string
529
+     *
530
+     * @return string Encoded HTML identifier string
531
+     */
532
+    static function dn_encode($str)
533
+    {
534
+        return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
535
+    }
536
+
537
+    /**
538
+     * Decodes DN string encoded with _dn_encode()
539
+     *
540
+     * @param string $str Encoded HTML identifier string
541
+     *
542
+     * @return string DN string
543
+     */
544
+    static function dn_decode($str)
545
+    {
546
+        $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
547
+        return base64_decode($str);
548
+    }
549
+}
550
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization/bg_BG.inc Added
12
 
1
@@ -0,0 +1,10 @@
2
+<?php
3
+/**
4
+ * Localizations for the Kolab Auth plugin
5
+ *
6
+ * Copyright (C) 2014, Kolab Systems AG
7
+ *
8
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
9
+ */
10
+$labels['loginas'] = 'Влизане като';
11
+?>
12
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization/de_CH.inc Added
12
 
1
@@ -0,0 +1,10 @@
2
+<?php
3
+/**
4
+ * Localizations for the Kolab Auth plugin
5
+ *
6
+ * Copyright (C) 2014, Kolab Systems AG
7
+ *
8
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
9
+ */
10
+$labels['loginas'] = 'Anmelden als';
11
+?>
12
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization/de_DE.inc Added
13
 
1
@@ -0,0 +1,11 @@
2
+<?php
3
+/**
4
+ * Localizations for the Kolab Auth plugin
5
+ *
6
+ * Copyright (C) 2014, Kolab Systems AG
7
+ *
8
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
9
+ */
10
+$labels['loginas'] = 'Anmelden als';
11
+$labels['loginasnotallowed'] = 'Keine Privilegien zum Anmelden als $user';
12
+?>
13
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization/en_US.inc Added
16
 
1
@@ -0,0 +1,14 @@
2
+<?php
3
+
4
+/**
5
+ * Localizations for the Kolab Auth plugin
6
+ *
7
+ * Copyright (C) 2014, Kolab Systems AG
8
+ *
9
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
10
+ */
11
+
12
+$labels['loginas'] = 'Login As';
13
+$labels['loginasnotallowed'] = 'No privileges to login as $user';
14
+
15
+?>
16
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization/es_ES.inc Added
11
 
1
@@ -0,0 +1,9 @@
2
+<?php
3
+/**
4
+ * Localizations for the Kolab Auth plugin
5
+ *
6
+ * Copyright (C) 2014, Kolab Systems AG
7
+ *
8
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
9
+ */
10
+?>
11
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization/et_EE.inc Added
11
 
1
@@ -0,0 +1,9 @@
2
+<?php
3
+/**
4
+ * Localizations for the Kolab Auth plugin
5
+ *
6
+ * Copyright (C) 2014, Kolab Systems AG
7
+ *
8
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
9
+ */
10
+?>
11
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization/fr_FR.inc Added
13
 
1
@@ -0,0 +1,11 @@
2
+<?php
3
+/**
4
+ * Localizations for the Kolab Auth plugin
5
+ *
6
+ * Copyright (C) 2014, Kolab Systems AG
7
+ *
8
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
9
+ */
10
+$labels['loginas'] = 'Se connecter en tant que';
11
+$labels['loginasnotallowed'] = 'Pas de privilège de se connecter comme $utilisateur';
12
+?>
13
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization/ja_JP.inc Added
12
 
1
@@ -0,0 +1,10 @@
2
+<?php
3
+/**
4
+ * Localizations for the Kolab Auth plugin
5
+ *
6
+ * Copyright (C) 2014, Kolab Systems AG
7
+ *
8
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
9
+ */
10
+$labels['loginas'] = 'ログイン';
11
+?>
12
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization/nl_NL.inc Added
12
 
1
@@ -0,0 +1,10 @@
2
+<?php
3
+/**
4
+ * Localizations for the Kolab Auth plugin
5
+ *
6
+ * Copyright (C) 2014, Kolab Systems AG
7
+ *
8
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
9
+ */
10
+$labels['loginas'] = 'Log in als';
11
+?>
12
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization/pl_PL.inc Added
12
 
1
@@ -0,0 +1,10 @@
2
+<?php
3
+/**
4
+ * Localizations for the Kolab Auth plugin
5
+ *
6
+ * Copyright (C) 2014, Kolab Systems AG
7
+ *
8
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
9
+ */
10
+$labels['loginas'] = 'Zaloguj jako';
11
+?>
12
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization/pt_BR.inc Added
12
 
1
@@ -0,0 +1,10 @@
2
+<?php
3
+/**
4
+ * Localizations for the Kolab Auth plugin
5
+ *
6
+ * Copyright (C) 2014, Kolab Systems AG
7
+ *
8
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
9
+ */
10
+$labels['loginas'] = 'Logar como';
11
+?>
12
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/kolab_auth/localization/ru_RU.inc Added
13
 
1
@@ -0,0 +1,11 @@
2
+<?php
3
+/**
4
+ * Localizations for the Kolab Auth plugin
5
+ *
6
+ * Copyright (C) 2014, Kolab Systems AG
7
+ *
8
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
9
+ */
10
+$labels['loginas'] = 'Войти как';
11
+$labels['loginasnotallowed'] = 'Нет привилегий войти как $user';
12
+?>
13
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/LICENSE Added
663
 
1
@@ -0,0 +1,661 @@
2
+                    GNU AFFERO GENERAL PUBLIC LICENSE
3
+                       Version 3, 19 November 2007
4
+
5
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
6
+ Everyone is permitted to copy and distribute verbatim copies
7
+ of this license document, but changing it is not allowed.
8
+
9
+                            Preamble
10
+
11
+  The GNU Affero General Public License is a free, copyleft license for
12
+software and other kinds of works, specifically designed to ensure
13
+cooperation with the community in the case of network server software.
14
+
15
+  The licenses for most software and other practical works are designed
16
+to take away your freedom to share and change the works.  By contrast,
17
+our General Public Licenses are intended to guarantee your freedom to
18
+share and change all versions of a program--to make sure it remains free
19
+software for all its users.
20
+
21
+  When we speak of free software, we are referring to freedom, not
22
+price.  Our General Public Licenses are designed to make sure that you
23
+have the freedom to distribute copies of free software (and charge for
24
+them if you wish), that you receive source code or can get it if you
25
+want it, that you can change the software or use pieces of it in new
26
+free programs, and that you know you can do these things.
27
+
28
+  Developers that use our General Public Licenses protect your rights
29
+with two steps: (1) assert copyright on the software, and (2) offer
30
+you this License which gives you legal permission to copy, distribute
31
+and/or modify the software.
32
+
33
+  A secondary benefit of defending all users' freedom is that
34
+improvements made in alternate versions of the program, if they
35
+receive widespread use, become available for other developers to
36
+incorporate.  Many developers of free software are heartened and
37
+encouraged by the resulting cooperation.  However, in the case of
38
+software used on network servers, this result may fail to come about.
39
+The GNU General Public License permits making a modified version and
40
+letting the public access it on a server without ever releasing its
41
+source code to the public.
42
+
43
+  The GNU Affero General Public License is designed specifically to
44
+ensure that, in such cases, the modified source code becomes available
45
+to the community.  It requires the operator of a network server to
46
+provide the source code of the modified version running there to the
47
+users of that server.  Therefore, public use of a modified version, on
48
+a publicly accessible server, gives the public access to the source
49
+code of the modified version.
50
+
51
+  An older license, called the Affero General Public License and
52
+published by Affero, was designed to accomplish similar goals.  This is
53
+a different license, not a version of the Affero GPL, but Affero has
54
+released a new version of the Affero GPL which permits relicensing under
55
+this license.
56
+
57
+  The precise terms and conditions for copying, distribution and
58
+modification follow.
59
+
60
+                       TERMS AND CONDITIONS
61
+
62
+  0. Definitions.
63
+
64
+  "This License" refers to version 3 of the GNU Affero General Public License.
65
+
66
+  "Copyright" also means copyright-like laws that apply to other kinds of
67
+works, such as semiconductor masks.
68
+
69
+  "The Program" refers to any copyrightable work licensed under this
70
+License.  Each licensee is addressed as "you".  "Licensees" and
71
+"recipients" may be individuals or organizations.
72
+
73
+  To "modify" a work means to copy from or adapt all or part of the work
74
+in a fashion requiring copyright permission, other than the making of an
75
+exact copy.  The resulting work is called a "modified version" of the
76
+earlier work or a work "based on" the earlier work.
77
+
78
+  A "covered work" means either the unmodified Program or a work based
79
+on the Program.
80
+
81
+  To "propagate" a work means to do anything with it that, without
82
+permission, would make you directly or secondarily liable for
83
+infringement under applicable copyright law, except executing it on a
84
+computer or modifying a private copy.  Propagation includes copying,
85
+distribution (with or without modification), making available to the
86
+public, and in some countries other activities as well.
87
+
88
+  To "convey" a work means any kind of propagation that enables other
89
+parties to make or receive copies.  Mere interaction with a user through
90
+a computer network, with no transfer of a copy, is not conveying.
91
+
92
+  An interactive user interface displays "Appropriate Legal Notices"
93
+to the extent that it includes a convenient and prominently visible
94
+feature that (1) displays an appropriate copyright notice, and (2)
95
+tells the user that there is no warranty for the work (except to the
96
+extent that warranties are provided), that licensees may convey the
97
+work under this License, and how to view a copy of this License.  If
98
+the interface presents a list of user commands or options, such as a
99
+menu, a prominent item in the list meets this criterion.
100
+
101
+  1. Source Code.
102
+
103
+  The "source code" for a work means the preferred form of the work
104
+for making modifications to it.  "Object code" means any non-source
105
+form of a work.
106
+
107
+  A "Standard Interface" means an interface that either is an official
108
+standard defined by a recognized standards body, or, in the case of
109
+interfaces specified for a particular programming language, one that
110
+is widely used among developers working in that language.
111
+
112
+  The "System Libraries" of an executable work include anything, other
113
+than the work as a whole, that (a) is included in the normal form of
114
+packaging a Major Component, but which is not part of that Major
115
+Component, and (b) serves only to enable use of the work with that
116
+Major Component, or to implement a Standard Interface for which an
117
+implementation is available to the public in source code form.  A
118
+"Major Component", in this context, means a major essential component
119
+(kernel, window system, and so on) of the specific operating system
120
+(if any) on which the executable work runs, or a compiler used to
121
+produce the work, or an object code interpreter used to run it.
122
+
123
+  The "Corresponding Source" for a work in object code form means all
124
+the source code needed to generate, install, and (for an executable
125
+work) run the object code and to modify the work, including scripts to
126
+control those activities.  However, it does not include the work's
127
+System Libraries, or general-purpose tools or generally available free
128
+programs which are used unmodified in performing those activities but
129
+which are not part of the work.  For example, Corresponding Source
130
+includes interface definition files associated with source files for
131
+the work, and the source code for shared libraries and dynamically
132
+linked subprograms that the work is specifically designed to require,
133
+such as by intimate data communication or control flow between those
134
+subprograms and other parts of the work.
135
+
136
+  The Corresponding Source need not include anything that users
137
+can regenerate automatically from other parts of the Corresponding
138
+Source.
139
+
140
+  The Corresponding Source for a work in source code form is that
141
+same work.
142
+
143
+  2. Basic Permissions.
144
+
145
+  All rights granted under this License are granted for the term of
146
+copyright on the Program, and are irrevocable provided the stated
147
+conditions are met.  This License explicitly affirms your unlimited
148
+permission to run the unmodified Program.  The output from running a
149
+covered work is covered by this License only if the output, given its
150
+content, constitutes a covered work.  This License acknowledges your
151
+rights of fair use or other equivalent, as provided by copyright law.
152
+
153
+  You may make, run and propagate covered works that you do not
154
+convey, without conditions so long as your license otherwise remains
155
+in force.  You may convey covered works to others for the sole purpose
156
+of having them make modifications exclusively for you, or provide you
157
+with facilities for running those works, provided that you comply with
158
+the terms of this License in conveying all material for which you do
159
+not control copyright.  Those thus making or running the covered works
160
+for you must do so exclusively on your behalf, under your direction
161
+and control, on terms that prohibit them from making any copies of
162
+your copyrighted material outside their relationship with you.
163
+
164
+  Conveying under any other circumstances is permitted solely under
165
+the conditions stated below.  Sublicensing is not allowed; section 10
166
+makes it unnecessary.
167
+
168
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
169
+
170
+  No covered work shall be deemed part of an effective technological
171
+measure under any applicable law fulfilling obligations under article
172
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
173
+similar laws prohibiting or restricting circumvention of such
174
+measures.
175
+
176
+  When you convey a covered work, you waive any legal power to forbid
177
+circumvention of technological measures to the extent such circumvention
178
+is effected by exercising rights under this License with respect to
179
+the covered work, and you disclaim any intention to limit operation or
180
+modification of the work as a means of enforcing, against the work's
181
+users, your or third parties' legal rights to forbid circumvention of
182
+technological measures.
183
+
184
+  4. Conveying Verbatim Copies.
185
+
186
+  You may convey verbatim copies of the Program's source code as you
187
+receive it, in any medium, provided that you conspicuously and
188
+appropriately publish on each copy an appropriate copyright notice;
189
+keep intact all notices stating that this License and any
190
+non-permissive terms added in accord with section 7 apply to the code;
191
+keep intact all notices of the absence of any warranty; and give all
192
+recipients a copy of this License along with the Program.
193
+
194
+  You may charge any price or no price for each copy that you convey,
195
+and you may offer support or warranty protection for a fee.
196
+
197
+  5. Conveying Modified Source Versions.
198
+
199
+  You may convey a work based on the Program, or the modifications to
200
+produce it from the Program, in the form of source code under the
201
+terms of section 4, provided that you also meet all of these conditions:
202
+
203
+    a) The work must carry prominent notices stating that you modified
204
+    it, and giving a relevant date.
205
+
206
+    b) The work must carry prominent notices stating that it is
207
+    released under this License and any conditions added under section
208
+    7.  This requirement modifies the requirement in section 4 to
209
+    "keep intact all notices".
210
+
211
+    c) You must license the entire work, as a whole, under this
212
+    License to anyone who comes into possession of a copy.  This
213
+    License will therefore apply, along with any applicable section 7
214
+    additional terms, to the whole of the work, and all its parts,
215
+    regardless of how they are packaged.  This License gives no
216
+    permission to license the work in any other way, but it does not
217
+    invalidate such permission if you have separately received it.
218
+
219
+    d) If the work has interactive user interfaces, each must display
220
+    Appropriate Legal Notices; however, if the Program has interactive
221
+    interfaces that do not display Appropriate Legal Notices, your
222
+    work need not make them do so.
223
+
224
+  A compilation of a covered work with other separate and independent
225
+works, which are not by their nature extensions of the covered work,
226
+and which are not combined with it such as to form a larger program,
227
+in or on a volume of a storage or distribution medium, is called an
228
+"aggregate" if the compilation and its resulting copyright are not
229
+used to limit the access or legal rights of the compilation's users
230
+beyond what the individual works permit.  Inclusion of a covered work
231
+in an aggregate does not cause this License to apply to the other
232
+parts of the aggregate.
233
+
234
+  6. Conveying Non-Source Forms.
235
+
236
+  You may convey a covered work in object code form under the terms
237
+of sections 4 and 5, provided that you also convey the
238
+machine-readable Corresponding Source under the terms of this License,
239
+in one of these ways:
240
+
241
+    a) Convey the object code in, or embodied in, a physical product
242
+    (including a physical distribution medium), accompanied by the
243
+    Corresponding Source fixed on a durable physical medium
244
+    customarily used for software interchange.
245
+
246
+    b) Convey the object code in, or embodied in, a physical product
247
+    (including a physical distribution medium), accompanied by a
248
+    written offer, valid for at least three years and valid for as
249
+    long as you offer spare parts or customer support for that product
250
+    model, to give anyone who possesses the object code either (1) a
251
+    copy of the Corresponding Source for all the software in the
252
+    product that is covered by this License, on a durable physical
253
+    medium customarily used for software interchange, for a price no
254
+    more than your reasonable cost of physically performing this
255
+    conveying of source, or (2) access to copy the
256
+    Corresponding Source from a network server at no charge.
257
+
258
+    c) Convey individual copies of the object code with a copy of the
259
+    written offer to provide the Corresponding Source.  This
260
+    alternative is allowed only occasionally and noncommercially, and
261
+    only if you received the object code with such an offer, in accord
262
+    with subsection 6b.
263
+
264
+    d) Convey the object code by offering access from a designated
265
+    place (gratis or for a charge), and offer equivalent access to the
266
+    Corresponding Source in the same way through the same place at no
267
+    further charge.  You need not require recipients to copy the
268
+    Corresponding Source along with the object code.  If the place to
269
+    copy the object code is a network server, the Corresponding Source
270
+    may be on a different server (operated by you or a third party)
271
+    that supports equivalent copying facilities, provided you maintain
272
+    clear directions next to the object code saying where to find the
273
+    Corresponding Source.  Regardless of what server hosts the
274
+    Corresponding Source, you remain obligated to ensure that it is
275
+    available for as long as needed to satisfy these requirements.
276
+
277
+    e) Convey the object code using peer-to-peer transmission, provided
278
+    you inform other peers where the object code and Corresponding
279
+    Source of the work are being offered to the general public at no
280
+    charge under subsection 6d.
281
+
282
+  A separable portion of the object code, whose source code is excluded
283
+from the Corresponding Source as a System Library, need not be
284
+included in conveying the object code work.
285
+
286
+  A "User Product" is either (1) a "consumer product", which means any
287
+tangible personal property which is normally used for personal, family,
288
+or household purposes, or (2) anything designed or sold for incorporation
289
+into a dwelling.  In determining whether a product is a consumer product,
290
+doubtful cases shall be resolved in favor of coverage.  For a particular
291
+product received by a particular user, "normally used" refers to a
292
+typical or common use of that class of product, regardless of the status
293
+of the particular user or of the way in which the particular user
294
+actually uses, or expects or is expected to use, the product.  A product
295
+is a consumer product regardless of whether the product has substantial
296
+commercial, industrial or non-consumer uses, unless such uses represent
297
+the only significant mode of use of the product.
298
+
299
+  "Installation Information" for a User Product means any methods,
300
+procedures, authorization keys, or other information required to install
301
+and execute modified versions of a covered work in that User Product from
302
+a modified version of its Corresponding Source.  The information must
303
+suffice to ensure that the continued functioning of the modified object
304
+code is in no case prevented or interfered with solely because
305
+modification has been made.
306
+
307
+  If you convey an object code work under this section in, or with, or
308
+specifically for use in, a User Product, and the conveying occurs as
309
+part of a transaction in which the right of possession and use of the
310
+User Product is transferred to the recipient in perpetuity or for a
311
+fixed term (regardless of how the transaction is characterized), the
312
+Corresponding Source conveyed under this section must be accompanied
313
+by the Installation Information.  But this requirement does not apply
314
+if neither you nor any third party retains the ability to install
315
+modified object code on the User Product (for example, the work has
316
+been installed in ROM).
317
+
318
+  The requirement to provide Installation Information does not include a
319
+requirement to continue to provide support service, warranty, or updates
320
+for a work that has been modified or installed by the recipient, or for
321
+the User Product in which it has been modified or installed.  Access to a
322
+network may be denied when the modification itself materially and
323
+adversely affects the operation of the network or violates the rules and
324
+protocols for communication across the network.
325
+
326
+  Corresponding Source conveyed, and Installation Information provided,
327
+in accord with this section must be in a format that is publicly
328
+documented (and with an implementation available to the public in
329
+source code form), and must require no special password or key for
330
+unpacking, reading or copying.
331
+
332
+  7. Additional Terms.
333
+
334
+  "Additional permissions" are terms that supplement the terms of this
335
+License by making exceptions from one or more of its conditions.
336
+Additional permissions that are applicable to the entire Program shall
337
+be treated as though they were included in this License, to the extent
338
+that they are valid under applicable law.  If additional permissions
339
+apply only to part of the Program, that part may be used separately
340
+under those permissions, but the entire Program remains governed by
341
+this License without regard to the additional permissions.
342
+
343
+  When you convey a copy of a covered work, you may at your option
344
+remove any additional permissions from that copy, or from any part of
345
+it.  (Additional permissions may be written to require their own
346
+removal in certain cases when you modify the work.)  You may place
347
+additional permissions on material, added by you to a covered work,
348
+for which you have or can give appropriate copyright permission.
349
+
350
+  Notwithstanding any other provision of this License, for material you
351
+add to a covered work, you may (if authorized by the copyright holders of
352
+that material) supplement the terms of this License with terms:
353
+
354
+    a) Disclaiming warranty or limiting liability differently from the
355
+    terms of sections 15 and 16 of this License; or
356
+
357
+    b) Requiring preservation of specified reasonable legal notices or
358
+    author attributions in that material or in the Appropriate Legal
359
+    Notices displayed by works containing it; or
360
+
361
+    c) Prohibiting misrepresentation of the origin of that material, or
362
+    requiring that modified versions of such material be marked in
363
+    reasonable ways as different from the original version; or
364
+
365
+    d) Limiting the use for publicity purposes of names of licensors or
366
+    authors of the material; or
367
+
368
+    e) Declining to grant rights under trademark law for use of some
369
+    trade names, trademarks, or service marks; or
370
+
371
+    f) Requiring indemnification of licensors and authors of that
372
+    material by anyone who conveys the material (or modified versions of
373
+    it) with contractual assumptions of liability to the recipient, for
374
+    any liability that these contractual assumptions directly impose on
375
+    those licensors and authors.
376
+
377
+  All other non-permissive additional terms are considered "further
378
+restrictions" within the meaning of section 10.  If the Program as you
379
+received it, or any part of it, contains a notice stating that it is
380
+governed by this License along with a term that is a further
381
+restriction, you may remove that term.  If a license document contains
382
+a further restriction but permits relicensing or conveying under this
383
+License, you may add to a covered work material governed by the terms
384
+of that license document, provided that the further restriction does
385
+not survive such relicensing or conveying.
386
+
387
+  If you add terms to a covered work in accord with this section, you
388
+must place, in the relevant source files, a statement of the
389
+additional terms that apply to those files, or a notice indicating
390
+where to find the applicable terms.
391
+
392
+  Additional terms, permissive or non-permissive, may be stated in the
393
+form of a separately written license, or stated as exceptions;
394
+the above requirements apply either way.
395
+
396
+  8. Termination.
397
+
398
+  You may not propagate or modify a covered work except as expressly
399
+provided under this License.  Any attempt otherwise to propagate or
400
+modify it is void, and will automatically terminate your rights under
401
+this License (including any patent licenses granted under the third
402
+paragraph of section 11).
403
+
404
+  However, if you cease all violation of this License, then your
405
+license from a particular copyright holder is reinstated (a)
406
+provisionally, unless and until the copyright holder explicitly and
407
+finally terminates your license, and (b) permanently, if the copyright
408
+holder fails to notify you of the violation by some reasonable means
409
+prior to 60 days after the cessation.
410
+
411
+  Moreover, your license from a particular copyright holder is
412
+reinstated permanently if the copyright holder notifies you of the
413
+violation by some reasonable means, this is the first time you have
414
+received notice of violation of this License (for any work) from that
415
+copyright holder, and you cure the violation prior to 30 days after
416
+your receipt of the notice.
417
+
418
+  Termination of your rights under this section does not terminate the
419
+licenses of parties who have received copies or rights from you under
420
+this License.  If your rights have been terminated and not permanently
421
+reinstated, you do not qualify to receive new licenses for the same
422
+material under section 10.
423
+
424
+  9. Acceptance Not Required for Having Copies.
425
+
426
+  You are not required to accept this License in order to receive or
427
+run a copy of the Program.  Ancillary propagation of a covered work
428
+occurring solely as a consequence of using peer-to-peer transmission
429
+to receive a copy likewise does not require acceptance.  However,
430
+nothing other than this License grants you permission to propagate or
431
+modify any covered work.  These actions infringe copyright if you do
432
+not accept this License.  Therefore, by modifying or propagating a
433
+covered work, you indicate your acceptance of this License to do so.
434
+
435
+  10. Automatic Licensing of Downstream Recipients.
436
+
437
+  Each time you convey a covered work, the recipient automatically
438
+receives a license from the original licensors, to run, modify and
439
+propagate that work, subject to this License.  You are not responsible
440
+for enforcing compliance by third parties with this License.
441
+
442
+  An "entity transaction" is a transaction transferring control of an
443
+organization, or substantially all assets of one, or subdividing an
444
+organization, or merging organizations.  If propagation of a covered
445
+work results from an entity transaction, each party to that
446
+transaction who receives a copy of the work also receives whatever
447
+licenses to the work the party's predecessor in interest had or could
448
+give under the previous paragraph, plus a right to possession of the
449
+Corresponding Source of the work from the predecessor in interest, if
450
+the predecessor has it or can get it with reasonable efforts.
451
+
452
+  You may not impose any further restrictions on the exercise of the
453
+rights granted or affirmed under this License.  For example, you may
454
+not impose a license fee, royalty, or other charge for exercise of
455
+rights granted under this License, and you may not initiate litigation
456
+(including a cross-claim or counterclaim in a lawsuit) alleging that
457
+any patent claim is infringed by making, using, selling, offering for
458
+sale, or importing the Program or any portion of it.
459
+
460
+  11. Patents.
461
+
462
+  A "contributor" is a copyright holder who authorizes use under this
463
+License of the Program or a work on which the Program is based.  The
464
+work thus licensed is called the contributor's "contributor version".
465
+
466
+  A contributor's "essential patent claims" are all patent claims
467
+owned or controlled by the contributor, whether already acquired or
468
+hereafter acquired, that would be infringed by some manner, permitted
469
+by this License, of making, using, or selling its contributor version,
470
+but do not include claims that would be infringed only as a
471
+consequence of further modification of the contributor version.  For
472
+purposes of this definition, "control" includes the right to grant
473
+patent sublicenses in a manner consistent with the requirements of
474
+this License.
475
+
476
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
477
+patent license under the contributor's essential patent claims, to
478
+make, use, sell, offer for sale, import and otherwise run, modify and
479
+propagate the contents of its contributor version.
480
+
481
+  In the following three paragraphs, a "patent license" is any express
482
+agreement or commitment, however denominated, not to enforce a patent
483
+(such as an express permission to practice a patent or covenant not to
484
+sue for patent infringement).  To "grant" such a patent license to a
485
+party means to make such an agreement or commitment not to enforce a
486
+patent against the party.
487
+
488
+  If you convey a covered work, knowingly relying on a patent license,
489
+and the Corresponding Source of the work is not available for anyone
490
+to copy, free of charge and under the terms of this License, through a
491
+publicly available network server or other readily accessible means,
492
+then you must either (1) cause the Corresponding Source to be so
493
+available, or (2) arrange to deprive yourself of the benefit of the
494
+patent license for this particular work, or (3) arrange, in a manner
495
+consistent with the requirements of this License, to extend the patent
496
+license to downstream recipients.  "Knowingly relying" means you have
497
+actual knowledge that, but for the patent license, your conveying the
498
+covered work in a country, or your recipient's use of the covered work
499
+in a country, would infringe one or more identifiable patents in that
500
+country that you have reason to believe are valid.
501
+
502
+  If, pursuant to or in connection with a single transaction or
503
+arrangement, you convey, or propagate by procuring conveyance of, a
504
+covered work, and grant a patent license to some of the parties
505
+receiving the covered work authorizing them to use, propagate, modify
506
+or convey a specific copy of the covered work, then the patent license
507
+you grant is automatically extended to all recipients of the covered
508
+work and works based on it.
509
+
510
+  A patent license is "discriminatory" if it does not include within
511
+the scope of its coverage, prohibits the exercise of, or is
512
+conditioned on the non-exercise of one or more of the rights that are
513
+specifically granted under this License.  You may not convey a covered
514
+work if you are a party to an arrangement with a third party that is
515
+in the business of distributing software, under which you make payment
516
+to the third party based on the extent of your activity of conveying
517
+the work, and under which the third party grants, to any of the
518
+parties who would receive the covered work from you, a discriminatory
519
+patent license (a) in connection with copies of the covered work
520
+conveyed by you (or copies made from those copies), or (b) primarily
521
+for and in connection with specific products or compilations that
522
+contain the covered work, unless you entered into that arrangement,
523
+or that patent license was granted, prior to 28 March 2007.
524
+
525
+  Nothing in this License shall be construed as excluding or limiting
526
+any implied license or other defenses to infringement that may
527
+otherwise be available to you under applicable patent law.
528
+
529
+  12. No Surrender of Others' Freedom.
530
+
531
+  If conditions are imposed on you (whether by court order, agreement or
532
+otherwise) that contradict the conditions of this License, they do not
533
+excuse you from the conditions of this License.  If you cannot convey a
534
+covered work so as to satisfy simultaneously your obligations under this
535
+License and any other pertinent obligations, then as a consequence you may
536
+not convey it at all.  For example, if you agree to terms that obligate you
537
+to collect a royalty for further conveying from those to whom you convey
538
+the Program, the only way you could satisfy both those terms and this
539
+License would be to refrain entirely from conveying the Program.
540
+
541
+  13. Remote Network Interaction; Use with the GNU General Public License.
542
+
543
+  Notwithstanding any other provision of this License, if you modify the
544
+Program, your modified version must prominently offer all users
545
+interacting with it remotely through a computer network (if your version
546
+supports such interaction) an opportunity to receive the Corresponding
547
+Source of your version by providing access to the Corresponding Source
548
+from a network server at no charge, through some standard or customary
549
+means of facilitating copying of software.  This Corresponding Source
550
+shall include the Corresponding Source for any work covered by version 3
551
+of the GNU General Public License that is incorporated pursuant to the
552
+following paragraph.
553
+
554
+  Notwithstanding any other provision of this License, you have
555
+permission to link or combine any covered work with a work licensed
556
+under version 3 of the GNU General Public License into a single
557
+combined work, and to convey the resulting work.  The terms of this
558
+License will continue to apply to the part which is the covered work,
559
+but the work with which it is combined will remain governed by version
560
+3 of the GNU General Public License.
561
+
562
+  14. Revised Versions of this License.
563
+
564
+  The Free Software Foundation may publish revised and/or new versions of
565
+the GNU Affero General Public License from time to time.  Such new versions
566
+will be similar in spirit to the present version, but may differ in detail to
567
+address new problems or concerns.
568
+
569
+  Each version is given a distinguishing version number.  If the
570
+Program specifies that a certain numbered version of the GNU Affero General
571
+Public License "or any later version" applies to it, you have the
572
+option of following the terms and conditions either of that numbered
573
+version or of any later version published by the Free Software
574
+Foundation.  If the Program does not specify a version number of the
575
+GNU Affero General Public License, you may choose any version ever published
576
+by the Free Software Foundation.
577
+
578
+  If the Program specifies that a proxy can decide which future
579
+versions of the GNU Affero General Public License can be used, that proxy's
580
+public statement of acceptance of a version permanently authorizes you
581
+to choose that version for the Program.
582
+
583
+  Later license versions may give you additional or different
584
+permissions.  However, no additional obligations are imposed on any
585
+author or copyright holder as a result of your choosing to follow a
586
+later version.
587
+
588
+  15. Disclaimer of Warranty.
589
+
590
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
591
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
592
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
593
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
594
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
595
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
596
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
597
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
598
+
599
+  16. Limitation of Liability.
600
+
601
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
602
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
603
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
604
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
605
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
606
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
607
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
608
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
609
+SUCH DAMAGES.
610
+
611
+  17. Interpretation of Sections 15 and 16.
612
+
613
+  If the disclaimer of warranty and limitation of liability provided
614
+above cannot be given local legal effect according to their terms,
615
+reviewing courts shall apply local law that most closely approximates
616
+an absolute waiver of all civil liability in connection with the
617
+Program, unless a warranty or assumption of liability accompanies a
618
+copy of the Program in return for a fee.
619
+
620
+                     END OF TERMS AND CONDITIONS
621
+
622
+            How to Apply These Terms to Your New Programs
623
+
624
+  If you develop a new program, and you want it to be of the greatest
625
+possible use to the public, the best way to achieve this is to make it
626
+free software which everyone can redistribute and change under these terms.
627
+
628
+  To do so, attach the following notices to the program.  It is safest
629
+to attach them to the start of each source file to most effectively
630
+state the exclusion of warranty; and each file should have at least
631
+the "copyright" line and a pointer to where the full notice is found.
632
+
633
+    <one line to give the program's name and a brief idea of what it does.>
634
+    Copyright (C) <year>  <name of author>
635
+
636
+    This program is free software: you can redistribute it and/or modify
637
+    it under the terms of the GNU Affero General Public License as published by
638
+    the Free Software Foundation, either version 3 of the License, or
639
+    (at your option) any later version.
640
+
641
+    This program is distributed in the hope that it will be useful,
642
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
643
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
644
+    GNU Affero General Public License for more details.
645
+
646
+    You should have received a copy of the GNU Affero General Public License
647
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
648
+
649
+Also add information on how to contact you by electronic and paper mail.
650
+
651
+  If your software can interact with users remotely through a computer
652
+network, you should also make sure that it provides a way for users to
653
+get its source.  For example, if your program is a web application, its
654
+interface could display a "Source" link that leads users to an archive
655
+of the code.  There are many ways you could offer source, and different
656
+solutions will be better for different programs; see section 13 for the
657
+specific requirements.
658
+
659
+  You should also get your employer (if you work as a programmer) or school,
660
+if any, to sign a "copyright disclaimer" for the program, if necessary.
661
+For more information on this, and how to apply and follow the GNU AGPL, see
662
+<http://www.gnu.org/licenses/>.
663
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/README Added
30
 
1
@@ -0,0 +1,28 @@
2
+libkolab plugin to access to Kolab groupware data
3
+=================================================
4
+
5
+The contained library classes establish a connection to the Kolab server
6
+and manage the access to the Kolab groupware objects stored in various
7
+IMAP folders. For reading and writing these objects, the PHP bindings of
8
+the libkolabxml library are used.
9
+
10
+
11
+REQUIREMENTS
12
+------------
13
+* libkolabxml PHP bindings
14
+  - kolabformat.so loaded into PHP
15
+  - kolabformat.php placed somewhere in the include_path
16
+* PEAR: HTTP/Request2
17
+* PEAR: Net/URL2
18
+
19
+
20
+INSTALLATION
21
+------------
22
+To use local cache you need to create a dedicated table in Roundcube's database.
23
+To do so, execute the SQL commands in SQL/<yourdatabase>.initial.sql
24
+
25
+
26
+CONFIGURATION
27
+-------------
28
+Rename config.inc.php.dist to config.inc.php in the plugin folder.
29
+For available configuration options see config.inc.php.dist file.
30
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/mysql Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/mysql.initial.sql Added
189
 
1
@@ -0,0 +1,187 @@
2
+/**
3
+ * libkolab database schema
4
+ *
5
+ * @version 1.1
6
+ * @author Thomas Bruederli
7
+ * @licence GNU AGPL
8
+ **/
9
+
10
+
11
+DROP TABLE IF EXISTS `kolab_folders`;
12
+
13
+CREATE TABLE `kolab_folders` (
14
+  `folder_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
15
+  `resource` VARCHAR(255) NOT NULL,
16
+  `type` VARCHAR(32) NOT NULL,
17
+  `synclock` INT(10) NOT NULL DEFAULT '0',
18
+  `ctag` VARCHAR(40) DEFAULT NULL,
19
+  PRIMARY KEY(`folder_id`),
20
+  INDEX `resource_type` (`resource`, `type`)
21
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
22
+
23
+DROP TABLE IF EXISTS `kolab_cache`;
24
+
25
+DROP TABLE IF EXISTS `kolab_cache_contact`;
26
+
27
+CREATE TABLE `kolab_cache_contact` (
28
+  `folder_id` BIGINT UNSIGNED NOT NULL,
29
+  `msguid` BIGINT UNSIGNED NOT NULL,
30
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
31
+  `created` DATETIME DEFAULT NULL,
32
+  `changed` DATETIME DEFAULT NULL,
33
+  `data` LONGTEXT NOT NULL,
34
+  `xml` LONGBLOB NOT NULL,
35
+  `tags` TEXT NOT NULL,
36
+  `words` TEXT NOT NULL,
37
+  `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
38
+  `name` VARCHAR(255) NOT NULL,
39
+  `firstname` VARCHAR(255) NOT NULL,
40
+  `surname` VARCHAR(255) NOT NULL,
41
+  `email` VARCHAR(255) NOT NULL,
42
+  CONSTRAINT `fk_kolab_cache_contact_folder` FOREIGN KEY (`folder_id`)
43
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
44
+  PRIMARY KEY(`folder_id`,`msguid`),
45
+  INDEX `contact_type` (`folder_id`,`type`),
46
+  INDEX `contact_uid2msguid` (`folder_id`,`uid`,`msguid`)
47
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
48
+
49
+DROP TABLE IF EXISTS `kolab_cache_event`;
50
+
51
+CREATE TABLE `kolab_cache_event` (
52
+  `folder_id` BIGINT UNSIGNED NOT NULL,
53
+  `msguid` BIGINT UNSIGNED NOT NULL,
54
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
55
+  `created` DATETIME DEFAULT NULL,
56
+  `changed` DATETIME DEFAULT NULL,
57
+  `data` LONGTEXT NOT NULL,
58
+  `xml` LONGBLOB NOT NULL,
59
+  `tags` TEXT NOT NULL,
60
+  `words` TEXT NOT NULL,
61
+  `dtstart` DATETIME,
62
+  `dtend` DATETIME,
63
+  CONSTRAINT `fk_kolab_cache_event_folder` FOREIGN KEY (`folder_id`)
64
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
65
+  PRIMARY KEY(`folder_id`,`msguid`),
66
+  INDEX `event_uid2msguid` (`folder_id`,`uid`,`msguid`)
67
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
68
+
69
+DROP TABLE IF EXISTS `kolab_cache_task`;
70
+
71
+CREATE TABLE `kolab_cache_task` (
72
+  `folder_id` BIGINT UNSIGNED NOT NULL,
73
+  `msguid` BIGINT UNSIGNED NOT NULL,
74
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
75
+  `created` DATETIME DEFAULT NULL,
76
+  `changed` DATETIME DEFAULT NULL,
77
+  `data` LONGTEXT NOT NULL,
78
+  `xml` LONGBLOB NOT NULL,
79
+  `tags` TEXT NOT NULL,
80
+  `words` TEXT NOT NULL,
81
+  `dtstart` DATETIME,
82
+  `dtend` DATETIME,
83
+  CONSTRAINT `fk_kolab_cache_task_folder` FOREIGN KEY (`folder_id`)
84
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
85
+  PRIMARY KEY(`folder_id`,`msguid`),
86
+  INDEX `task_uid2msguid` (`folder_id`,`uid`,`msguid`)
87
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
88
+
89
+DROP TABLE IF EXISTS `kolab_cache_journal`;
90
+
91
+CREATE TABLE `kolab_cache_journal` (
92
+  `folder_id` BIGINT UNSIGNED NOT NULL,
93
+  `msguid` BIGINT UNSIGNED NOT NULL,
94
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
95
+  `created` DATETIME DEFAULT NULL,
96
+  `changed` DATETIME DEFAULT NULL,
97
+  `data` LONGTEXT NOT NULL,
98
+  `xml` LONGBLOB NOT NULL,
99
+  `tags` TEXT NOT NULL,
100
+  `words` TEXT NOT NULL,
101
+  `dtstart` DATETIME,
102
+  `dtend` DATETIME,
103
+  CONSTRAINT `fk_kolab_cache_journal_folder` FOREIGN KEY (`folder_id`)
104
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
105
+  PRIMARY KEY(`folder_id`,`msguid`),
106
+  INDEX `journal_uid2msguid` (`folder_id`,`uid`,`msguid`)
107
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
108
+
109
+DROP TABLE IF EXISTS `kolab_cache_note`;
110
+
111
+CREATE TABLE `kolab_cache_note` (
112
+  `folder_id` BIGINT UNSIGNED NOT NULL,
113
+  `msguid` BIGINT UNSIGNED NOT NULL,
114
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
115
+  `created` DATETIME DEFAULT NULL,
116
+  `changed` DATETIME DEFAULT NULL,
117
+  `data` LONGTEXT NOT NULL,
118
+  `xml` LONGBLOB NOT NULL,
119
+  `tags` TEXT NOT NULL,
120
+  `words` TEXT NOT NULL,
121
+  CONSTRAINT `fk_kolab_cache_note_folder` FOREIGN KEY (`folder_id`)
122
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
123
+  PRIMARY KEY(`folder_id`,`msguid`),
124
+  INDEX `note_uid2msguid` (`folder_id`,`uid`,`msguid`)
125
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
126
+
127
+DROP TABLE IF EXISTS `kolab_cache_file`;
128
+
129
+CREATE TABLE `kolab_cache_file` (
130
+  `folder_id` BIGINT UNSIGNED NOT NULL,
131
+  `msguid` BIGINT UNSIGNED NOT NULL,
132
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
133
+  `created` DATETIME DEFAULT NULL,
134
+  `changed` DATETIME DEFAULT NULL,
135
+  `data` LONGTEXT NOT NULL,
136
+  `xml` LONGBLOB NOT NULL,
137
+  `tags` TEXT NOT NULL,
138
+  `words` TEXT NOT NULL,
139
+  `filename` varchar(255) DEFAULT NULL,
140
+  CONSTRAINT `fk_kolab_cache_file_folder` FOREIGN KEY (`folder_id`)
141
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
142
+  PRIMARY KEY(`folder_id`,`msguid`),
143
+  INDEX `folder_filename` (`folder_id`, `filename`),
144
+  INDEX `file_uid2msguid` (`folder_id`,`uid`,`msguid`)
145
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
146
+
147
+DROP TABLE IF EXISTS `kolab_cache_configuration`;
148
+
149
+CREATE TABLE `kolab_cache_configuration` (
150
+  `folder_id` BIGINT UNSIGNED NOT NULL,
151
+  `msguid` BIGINT UNSIGNED NOT NULL,
152
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
153
+  `created` DATETIME DEFAULT NULL,
154
+  `changed` DATETIME DEFAULT NULL,
155
+  `data` LONGTEXT NOT NULL,
156
+  `xml` LONGBLOB NOT NULL,
157
+  `tags` TEXT NOT NULL,
158
+  `words` TEXT NOT NULL,
159
+  `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
160
+  CONSTRAINT `fk_kolab_cache_configuration_folder` FOREIGN KEY (`folder_id`)
161
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
162
+  PRIMARY KEY(`folder_id`,`msguid`),
163
+  INDEX `configuration_type` (`folder_id`,`type`),
164
+  INDEX `configuration_uid2msguid` (`folder_id`,`uid`,`msguid`)
165
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
166
+
167
+DROP TABLE IF EXISTS `kolab_cache_freebusy`;
168
+
169
+CREATE TABLE `kolab_cache_freebusy` (
170
+  `folder_id` BIGINT UNSIGNED NOT NULL,
171
+  `msguid` BIGINT UNSIGNED NOT NULL,
172
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
173
+  `created` DATETIME DEFAULT NULL,
174
+  `changed` DATETIME DEFAULT NULL,
175
+  `data` LONGTEXT NOT NULL,
176
+  `xml` LONGBLOB NOT NULL,
177
+  `tags` TEXT NOT NULL,
178
+  `words` TEXT NOT NULL,
179
+  `dtstart` DATETIME,
180
+  `dtend` DATETIME,
181
+  CONSTRAINT `fk_kolab_cache_freebusy_folder` FOREIGN KEY (`folder_id`)
182
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
183
+  PRIMARY KEY(`folder_id`,`msguid`),
184
+  INDEX `freebusy_uid2msguid` (`folder_id`,`uid`,`msguid`)
185
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
186
+
187
+
188
+INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2015011600');
189
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/mysql/2013011000.sql Added
4
 
1
@@ -0,0 +1,1 @@
2
+-- empty
3
\ No newline at end of file
4
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/mysql/2013041900.sql Added
5
 
1
@@ -0,0 +1,3 @@
2
+DELETE FROM `kolab_cache` WHERE `type` = 'file';
3
+ALTER TABLE `kolab_cache` ADD `filename` varchar(255) DEFAULT NULL;
4
+ALTER TABLE `kolab_cache` ADD INDEX `resource_filename` (`resource`, `filename`);
5
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/mysql/2013100400.sql Added
176
 
1
@@ -0,0 +1,174 @@
2
+CREATE TABLE `kolab_folders` (
3
+  `folder_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
4
+  `resource` VARCHAR(255)  NOT NULL,
5
+  `type` VARCHAR(32) NOT NULL,
6
+  `synclock` INT(10) NOT NULL DEFAULT '0',
7
+  `ctag` VARCHAR(40) DEFAULT NULL,
8
+  PRIMARY KEY(`folder_id`),
9
+  INDEX `resource_type` (`resource`, `type`)
10
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
11
+
12
+CREATE TABLE `kolab_cache_contact` (
13
+  `folder_id` BIGINT UNSIGNED NOT NULL,
14
+  `msguid` BIGINT UNSIGNED NOT NULL,
15
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
16
+  `created` DATETIME DEFAULT NULL,
17
+  `changed` DATETIME DEFAULT NULL,
18
+  `data` TEXT NOT NULL,
19
+  `xml` TEXT NOT NULL,
20
+  `tags` VARCHAR(255) NOT NULL,
21
+  `words` TEXT NOT NULL,
22
+  `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
23
+  CONSTRAINT `fk_kolab_cache_contact_folder` FOREIGN KEY (`folder_id`)
24
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
25
+  PRIMARY KEY(`folder_id`,`msguid`),
26
+  INDEX `contact_type` (`folder_id`,`type`)
27
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
28
+
29
+CREATE TABLE `kolab_cache_event` (
30
+  `folder_id` BIGINT UNSIGNED NOT NULL,
31
+  `msguid` BIGINT UNSIGNED NOT NULL,
32
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
33
+  `created` DATETIME DEFAULT NULL,
34
+  `changed` DATETIME DEFAULT NULL,
35
+  `data` TEXT NOT NULL,
36
+  `xml` TEXT NOT NULL,
37
+  `tags` VARCHAR(255) NOT NULL,
38
+  `words` TEXT NOT NULL,
39
+  `dtstart` DATETIME,
40
+  `dtend` DATETIME,
41
+  CONSTRAINT `fk_kolab_cache_event_folder` FOREIGN KEY (`folder_id`)
42
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
43
+  PRIMARY KEY(`folder_id`,`msguid`)
44
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
45
+
46
+CREATE TABLE `kolab_cache_task` (
47
+  `folder_id` BIGINT UNSIGNED NOT NULL,
48
+  `msguid` BIGINT UNSIGNED NOT NULL,
49
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
50
+  `created` DATETIME DEFAULT NULL,
51
+  `changed` DATETIME DEFAULT NULL,
52
+  `data` TEXT NOT NULL,
53
+  `xml` TEXT NOT NULL,
54
+  `tags` VARCHAR(255) NOT NULL,
55
+  `words` TEXT NOT NULL,
56
+  `dtstart` DATETIME,
57
+  `dtend` DATETIME,
58
+  CONSTRAINT `fk_kolab_cache_task_folder` FOREIGN KEY (`folder_id`)
59
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
60
+  PRIMARY KEY(`folder_id`,`msguid`)
61
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
62
+
63
+CREATE TABLE `kolab_cache_journal` (
64
+  `folder_id` BIGINT UNSIGNED NOT NULL,
65
+  `msguid` BIGINT UNSIGNED NOT NULL,
66
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
67
+  `created` DATETIME DEFAULT NULL,
68
+  `changed` DATETIME DEFAULT NULL,
69
+  `data` TEXT NOT NULL,
70
+  `xml` TEXT NOT NULL,
71
+  `tags` VARCHAR(255) NOT NULL,
72
+  `words` TEXT NOT NULL,
73
+  `dtstart` DATETIME,
74
+  `dtend` DATETIME,
75
+  CONSTRAINT `fk_kolab_cache_journal_folder` FOREIGN KEY (`folder_id`)
76
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
77
+  PRIMARY KEY(`folder_id`,`msguid`)
78
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
79
+
80
+CREATE TABLE `kolab_cache_note` (
81
+  `folder_id` BIGINT UNSIGNED NOT NULL,
82
+  `msguid` BIGINT UNSIGNED NOT NULL,
83
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
84
+  `created` DATETIME DEFAULT NULL,
85
+  `changed` DATETIME DEFAULT NULL,
86
+  `data` TEXT NOT NULL,
87
+  `xml` TEXT NOT NULL,
88
+  `tags` VARCHAR(255) NOT NULL,
89
+  `words` TEXT NOT NULL,
90
+  CONSTRAINT `fk_kolab_cache_note_folder` FOREIGN KEY (`folder_id`)
91
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
92
+  PRIMARY KEY(`folder_id`,`msguid`)
93
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
94
+
95
+CREATE TABLE `kolab_cache_file` (
96
+  `folder_id` BIGINT UNSIGNED NOT NULL,
97
+  `msguid` BIGINT UNSIGNED NOT NULL,
98
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
99
+  `created` DATETIME DEFAULT NULL,
100
+  `changed` DATETIME DEFAULT NULL,
101
+  `data` TEXT NOT NULL,
102
+  `xml` TEXT NOT NULL,
103
+  `tags` VARCHAR(255) NOT NULL,
104
+  `words` TEXT NOT NULL,
105
+  `filename` varchar(255) DEFAULT NULL,
106
+  CONSTRAINT `fk_kolab_cache_file_folder` FOREIGN KEY (`folder_id`)
107
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
108
+  PRIMARY KEY(`folder_id`,`msguid`),
109
+  INDEX `folder_filename` (`folder_id`, `filename`)
110
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
111
+
112
+CREATE TABLE `kolab_cache_configuration` (
113
+  `folder_id` BIGINT UNSIGNED NOT NULL,
114
+  `msguid` BIGINT UNSIGNED NOT NULL,
115
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
116
+  `created` DATETIME DEFAULT NULL,
117
+  `changed` DATETIME DEFAULT NULL,
118
+  `data` TEXT NOT NULL,
119
+  `xml` TEXT NOT NULL,
120
+  `tags` VARCHAR(255) NOT NULL,
121
+  `words` TEXT NOT NULL,
122
+  `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
123
+  CONSTRAINT `fk_kolab_cache_configuration_folder` FOREIGN KEY (`folder_id`)
124
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
125
+  PRIMARY KEY(`folder_id`,`msguid`),
126
+  INDEX `configuration_type` (`folder_id`,`type`)
127
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
128
+
129
+CREATE TABLE `kolab_cache_freebusy` (
130
+  `folder_id` BIGINT UNSIGNED NOT NULL,
131
+  `msguid` BIGINT UNSIGNED NOT NULL,
132
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
133
+  `created` DATETIME DEFAULT NULL,
134
+  `changed` DATETIME DEFAULT NULL,
135
+  `data` TEXT NOT NULL,
136
+  `xml` TEXT NOT NULL,
137
+  `tags` VARCHAR(255) NOT NULL,
138
+  `words` TEXT NOT NULL,
139
+  `dtstart` DATETIME,
140
+  `dtend` DATETIME,
141
+  CONSTRAINT `fk_kolab_cache_freebusy_folder` FOREIGN KEY (`folder_id`)
142
+    REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
143
+  PRIMARY KEY(`folder_id`,`msguid`)
144
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
145
+
146
+
147
+-- Migrate data from old kolab_cache table
148
+
149
+INSERT INTO kolab_folders (resource, type)
150
+  SELECT DISTINCT resource, type
151
+  FROM  kolab_cache WHERE type IN ('event','contact','task','file');
152
+
153
+INSERT INTO kolab_cache_event (folder_id, msguid, uid, created, changed, data, xml, tags, words, dtstart, dtend)
154
+  SELECT kolab_folders.folder_id, msguid, uid, created, changed, data, xml, tags, words, dtstart, dtend
155
+  FROM kolab_cache LEFT JOIN kolab_folders ON (kolab_folders.resource = kolab_cache.resource)
156
+  WHERE kolab_cache.type = 'event' AND kolab_folders.folder_id IS NOT NULL;
157
+
158
+INSERT INTO kolab_cache_task (folder_id, msguid, uid, created, changed, data, xml, tags, words, dtstart, dtend)
159
+  SELECT kolab_folders.folder_id, msguid, uid, created, changed, data, xml, tags, words, dtstart, dtend
160
+  FROM kolab_cache LEFT JOIN kolab_folders ON (kolab_folders.resource = kolab_cache.resource)
161
+  WHERE kolab_cache.type = 'task' AND kolab_folders.folder_id IS NOT NULL;
162
+
163
+INSERT INTO kolab_cache_contact (folder_id, msguid, uid, created, changed, data, xml, tags, words, type)
164
+  SELECT kolab_folders.folder_id, msguid, uid, created, changed, data, xml, tags, words, kolab_cache.type
165
+  FROM kolab_cache LEFT JOIN kolab_folders ON (kolab_folders.resource = kolab_cache.resource)
166
+  WHERE kolab_cache.type IN ('contact','distribution-list') AND kolab_folders.folder_id IS NOT NULL;
167
+
168
+INSERT INTO kolab_cache_file (folder_id, msguid, uid, created, changed, data, xml, tags, words, filename)
169
+  SELECT kolab_folders.folder_id, msguid, uid, created, changed, data, xml, tags, words, filename
170
+  FROM kolab_cache LEFT JOIN kolab_folders ON (kolab_folders.resource = kolab_cache.resource)
171
+  WHERE kolab_cache.type = 'file' AND kolab_folders.folder_id IS NOT NULL;
172
+
173
+
174
+DROP TABLE IF EXISTS `kolab_cache`;
175
+
176
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/mysql/2013110400.sql Added
3
 
1
@@ -0,0 +1,1 @@
2
+ALTER TABLE `kolab_cache_contact` CHANGE `xml` `xml` LONGTEXT NOT NULL;
3
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/mysql/2013121100.sql Added
15
 
1
@@ -0,0 +1,13 @@
2
+-- well, these deletes are really optional
3
+-- we can clear all caches or only contacts/events/tasks
4
+-- the issue we're fixing here was about contacts (Bug #2662)
5
+DELETE FROM `kolab_folders` WHERE `type` IN ('contact', 'event', 'task');
6
+
7
+ALTER TABLE `kolab_cache_contact` CHANGE `xml` `xml` LONGBLOB NOT NULL;
8
+ALTER TABLE `kolab_cache_event` CHANGE `xml` `xml` LONGBLOB NOT NULL;
9
+ALTER TABLE `kolab_cache_task` CHANGE `xml` `xml` LONGBLOB NOT NULL;
10
+ALTER TABLE `kolab_cache_journal` CHANGE `xml` `xml` LONGBLOB NOT NULL;
11
+ALTER TABLE `kolab_cache_note` CHANGE `xml` `xml` LONGBLOB NOT NULL;
12
+ALTER TABLE `kolab_cache_file` CHANGE `xml` `xml` LONGBLOB NOT NULL;
13
+ALTER TABLE `kolab_cache_configuration` CHANGE `xml` `xml` LONGBLOB NOT NULL;
14
+ALTER TABLE `kolab_cache_freebusy` CHANGE `xml` `xml` LONGBLOB NOT NULL;
15
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/mysql/2014021000.sql Added
11
 
1
@@ -0,0 +1,9 @@
2
+ALTER TABLE `kolab_cache_contact` ADD `name` VARCHAR(255) NOT NULL,
3
+  ADD `firstname` VARCHAR(255) NOT NULL,
4
+  ADD `surname` VARCHAR(255) NOT NULL,
5
+  ADD `email` VARCHAR(255) NOT NULL;
6
+
7
+-- updating or clearing all contacts caches is required.
8
+-- either run `bin/modcache.sh update --type=contact` or execute the following query:
9
+--   DELETE FROM `kolab_folders` WHERE `type`='contact';
10
+
11
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/mysql/2014032700.sql Added
10
 
1
@@ -0,0 +1,8 @@
2
+ALTER TABLE `kolab_cache_configuration` ADD INDEX `configuration_uid2msguid` (`folder_id`, `uid`, `msguid`);
3
+ALTER TABLE `kolab_cache_contact` ADD INDEX `contact_uid2msguid` (`folder_id`, `uid`, `msguid`);
4
+ALTER TABLE `kolab_cache_event` ADD INDEX `event_uid2msguid` (`folder_id`, `uid`, `msguid`);
5
+ALTER TABLE `kolab_cache_task` ADD INDEX `task_uid2msguid` (`folder_id`, `uid`, `msguid`);
6
+ALTER TABLE `kolab_cache_journal` ADD INDEX `journal_uid2msguid` (`folder_id`, `uid`, `msguid`);
7
+ALTER TABLE `kolab_cache_note` ADD INDEX `note_uid2msguid` (`folder_id`, `uid`, `msguid`);
8
+ALTER TABLE `kolab_cache_file` ADD INDEX `file_uid2msguid` (`folder_id`, `uid`, `msguid`);
9
+ALTER TABLE `kolab_cache_freebusy` ADD INDEX `freebusy_uid2msguid` (`folder_id`, `uid`, `msguid`);
10
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/mysql/2014040900.sql Added
18
 
1
@@ -0,0 +1,16 @@
2
+ALTER TABLE `kolab_cache_contact` CHANGE `data` `data` LONGTEXT NOT NULL;
3
+ALTER TABLE `kolab_cache_event` CHANGE `data` `data` LONGTEXT NOT NULL;
4
+ALTER TABLE `kolab_cache_task` CHANGE `data` `data` LONGTEXT NOT NULL;
5
+ALTER TABLE `kolab_cache_journal` CHANGE `data` `data` LONGTEXT NOT NULL;
6
+ALTER TABLE `kolab_cache_note` CHANGE `data` `data` LONGTEXT NOT NULL;
7
+ALTER TABLE `kolab_cache_file` CHANGE `data` `data` LONGTEXT NOT NULL;
8
+ALTER TABLE `kolab_cache_configuration` CHANGE `data` `data` LONGTEXT NOT NULL;
9
+ALTER TABLE `kolab_cache_freebusy` CHANGE `data` `data` LONGTEXT NOT NULL;
10
+
11
+-- rebuild cache entries for xcal objects with alarms
12
+DELETE FROM `kolab_cache_event` WHERE tags LIKE '% x-has-alarms %';
13
+DELETE FROM `kolab_cache_task` WHERE tags LIKE '% x-has-alarms %';
14
+
15
+-- force cache synchronization
16
+UPDATE `kolab_folders` SET ctag='' WHERE `type` IN ('event','task');
17
+
18
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/mysql/2014112700.sql Added
4
 
1
@@ -0,0 +1,2 @@
2
+-- delete cache entries for old folder identifiers
3
+DELETE FROM `kolab_folders` WHERE `resource` LIKE 'imap://anonymous@%';
4
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/mysql/2015011600.sql Added
10
 
1
@@ -0,0 +1,8 @@
2
+ALTER TABLE `kolab_cache_contact` MODIFY `tags` text NOT NULL;
3
+ALTER TABLE `kolab_cache_event` MODIFY `tags` text NOT NULL;
4
+ALTER TABLE `kolab_cache_task` MODIFY `tags` text NOT NULL;
5
+ALTER TABLE `kolab_cache_journal` MODIFY `tags` text NOT NULL;
6
+ALTER TABLE `kolab_cache_note` MODIFY `tags` text NOT NULL;
7
+ALTER TABLE `kolab_cache_file` MODIFY `tags` text NOT NULL;
8
+ALTER TABLE `kolab_cache_configuration` MODIFY `tags` text NOT NULL;
9
+ALTER TABLE `kolab_cache_freebusy` MODIFY `tags` text NOT NULL;
10
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/oracle Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/oracle.initial.sql Added
186
 
1
@@ -0,0 +1,184 @@
2
+/**
3
+ * libkolab database schema
4
+ *
5
+ * @version 1.1
6
+ * @author Aleksander Machniak
7
+ * @licence GNU AGPL
8
+ **/
9
+
10
+
11
+CREATE TABLE "kolab_folders" (
12
+    "folder_id" number NOT NULL PRIMARY KEY,
13
+    "resource" VARCHAR(255) NOT NULL,
14
+    "type" VARCHAR(32) NOT NULL,
15
+    "synclock" integer DEFAULT 0 NOT NULL,
16
+    "ctag" VARCHAR(40) DEFAULT NULL
17
+);
18
+
19
+CREATE INDEX "kolab_folders_resource_idx" ON "kolab_folders" ("resource", "type");
20
+
21
+CREATE SEQUENCE "kolab_folders_seq"
22
+    START WITH 1 INCREMENT BY 1 NOMAXVALUE;
23
+
24
+CREATE TRIGGER "kolab_folders_seq_trig"
25
+BEFORE INSERT ON "kolab_folders" FOR EACH ROW
26
+BEGIN
27
+    :NEW."folder_id" := "kolab_folders_seq".nextval;
28
+END;
29
+/
30
+
31
+CREATE TABLE "kolab_cache_contact" (
32
+    "folder_id" number NOT NULL
33
+        REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
34
+    "msguid" number NOT NULL,
35
+    "uid" varchar(128) NOT NULL,
36
+    "created" timestamp DEFAULT NULL,
37
+    "changed" timestamp DEFAULT NULL,
38
+    "data" clob NOT NULL,
39
+    "xml" clob NOT NULL,
40
+    "tags" clob DEFAULT NULL,
41
+    "words" clob DEFAULT NULL,
42
+    "type" varchar(32) NOT NULL,
43
+    "name" varchar(255) DEFAULT NULL,
44
+    "firstname" varchar(255) DEFAULT NULL,
45
+    "surname" varchar(255) DEFAULT NULL,
46
+    "email" varchar(255) DEFAULT NULL,
47
+    PRIMARY KEY ("folder_id", "msguid")
48
+);
49
+
50
+CREATE INDEX "kolab_cache_contact_type_idx" ON "kolab_cache_contact" ("folder_id", "type");
51
+CREATE INDEX "kolab_cache_contact_uid2msguid" ON "kolab_cache_contact" ("folder_id", "uid", "msguid");
52
+
53
+
54
+CREATE TABLE "kolab_cache_event" (
55
+    "folder_id" number NOT NULL
56
+        REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
57
+    "msguid" number NOT NULL,
58
+    "uid" varchar(128) NOT NULL,
59
+    "created" timestamp DEFAULT NULL,
60
+    "changed" timestamp DEFAULT NULL,
61
+    "data" clob NOT NULL,
62
+    "xml" clob NOT NULL,
63
+    "tags" clob DEFAULT NULL,
64
+    "words" clob DEFAULT NULL,
65
+    "dtstart" timestamp DEFAULT NULL,
66
+    "dtend" timestamp DEFAULT NULL,
67
+    PRIMARY KEY ("folder_id", "msguid")
68
+);
69
+
70
+CREATE INDEX "kolab_cache_event_uid2msguid" ON "kolab_cache_event" ("folder_id", "uid", "msguid");
71
+
72
+
73
+CREATE TABLE "kolab_cache_task" (
74
+    "folder_id" number NOT NULL
75
+        REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
76
+    "msguid" number NOT NULL,
77
+    "uid" varchar(128) NOT NULL,
78
+    "created" timestamp DEFAULT NULL,
79
+    "changed" timestamp DEFAULT NULL,
80
+    "data" clob NOT NULL,
81
+    "xml" clob NOT NULL,
82
+    "tags" clob DEFAULT NULL,
83
+    "words" clob DEFAULT NULL,
84
+    "dtstart" timestamp DEFAULT NULL,
85
+    "dtend" timestamp DEFAULT NULL,
86
+    PRIMARY KEY ("folder_id", "msguid")
87
+);
88
+
89
+CREATE INDEX "kolab_cache_task_uid2msguid" ON "kolab_cache_task" ("folder_id", "uid", "msguid");
90
+
91
+
92
+CREATE TABLE "kolab_cache_journal" (
93
+    "folder_id" number NOT NULL
94
+        REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
95
+    "msguid" number NOT NULL,
96
+    "uid" varchar(128) NOT NULL,
97
+    "created" timestamp DEFAULT NULL,
98
+    "changed" timestamp DEFAULT NULL,
99
+    "data" clob NOT NULL,
100
+    "xml" clob NOT NULL,
101
+    "tags" clob DEFAULT NULL,
102
+    "words" clob DEFAULT NULL,
103
+    "dtstart" timestamp DEFAULT NULL,
104
+    "dtend" timestamp DEFAULT NULL,
105
+    PRIMARY KEY ("folder_id", "msguid")
106
+);
107
+
108
+CREATE INDEX "kolab_cache_journal_uid2msguid" ON "kolab_cache_journal" ("folder_id", "uid", "msguid");
109
+
110
+
111
+CREATE TABLE "kolab_cache_note" (
112
+    "folder_id" number NOT NULL
113
+        REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
114
+    "msguid" number NOT NULL,
115
+    "uid" varchar(128) NOT NULL,
116
+    "created" timestamp DEFAULT NULL,
117
+    "changed" timestamp DEFAULT NULL,
118
+    "data" clob NOT NULL,
119
+    "xml" clob NOT NULL,
120
+    "tags" clob DEFAULT NULL,
121
+    "words" clob DEFAULT NULL,
122
+    PRIMARY KEY ("folder_id", "msguid")
123
+);
124
+
125
+CREATE INDEX "kolab_cache_note_uid2msguid" ON "kolab_cache_note" ("folder_id", "uid", "msguid");
126
+
127
+
128
+CREATE TABLE "kolab_cache_file" (
129
+    "folder_id" number NOT NULL
130
+        REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
131
+    "msguid" number NOT NULL,
132
+    "uid" varchar(128) NOT NULL,
133
+    "created" timestamp DEFAULT NULL,
134
+    "changed" timestamp DEFAULT NULL,
135
+    "data" clob NOT NULL,
136
+    "xml" clob NOT NULL,
137
+    "tags" clob DEFAULT NULL,
138
+    "words" clob DEFAULT NULL,
139
+    "filename" varchar(255) DEFAULT NULL,
140
+    PRIMARY KEY ("folder_id", "msguid")
141
+);
142
+
143
+CREATE INDEX "kolab_cache_file_filename" ON "kolab_cache_file" ("folder_id", "filename");
144
+CREATE INDEX "kolab_cache_file_uid2msguid" ON "kolab_cache_file" ("folder_id", "uid", "msguid");
145
+
146
+
147
+CREATE TABLE "kolab_cache_configuration" (
148
+    "folder_id" number NOT NULL
149
+        REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
150
+    "msguid" number NOT NULL,
151
+    "uid" varchar(128) NOT NULL,
152
+    "created" timestamp DEFAULT NULL,
153
+    "changed" timestamp DEFAULT NULL,
154
+    "data" clob NOT NULL,
155
+    "xml" clob NOT NULL,
156
+    "tags" clob DEFAULT NULL,
157
+    "words" clob DEFAULT NULL,
158
+    "type" varchar(32) NOT NULL,
159
+    PRIMARY KEY ("folder_id", "msguid")
160
+);
161
+
162
+CREATE INDEX "kolab_cache_config_type" ON "kolab_cache_configuration" ("folder_id", "type");
163
+CREATE INDEX "kolab_cache_config_uid2msguid" ON "kolab_cache_configuration" ("folder_id", "uid", "msguid");
164
+
165
+
166
+CREATE TABLE "kolab_cache_freebusy" (
167
+    "folder_id" number NOT NULL
168
+        REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
169
+    "msguid" number NOT NULL,
170
+    "uid" varchar(128) NOT NULL,
171
+    "created" timestamp DEFAULT NULL,
172
+    "changed" timestamp DEFAULT NULL,
173
+    "data" clob NOT NULL,
174
+    "xml" clob NOT NULL,
175
+    "tags" clob DEFAULT NULL,
176
+    "words" clob DEFAULT NULL,
177
+    "dtstart" timestamp DEFAULT NULL,
178
+    "dtend" timestamp DEFAULT NULL,
179
+    PRIMARY KEY("folder_id", "msguid")
180
+);
181
+
182
+CREATE INDEX "kolab_cache_fb_uid2msguid" ON "kolab_cache_freebusy" ("folder_id", "uid", "msguid");
183
+
184
+
185
+INSERT INTO "system" ("name", "value") VALUES ('libkolab-version', '2015011600');
186
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/oracle/2015011600.sql Added
42
 
1
@@ -0,0 +1,40 @@
2
+-- direct change from varchar to clob does not work, need temp column (#4257)
3
+ALTER TABLE "kolab_cache_contact" ADD "tags1" clob DEFAULT NULL;
4
+UPDATE "kolab_cache_contact" SET "tags1" = "tags";
5
+ALTER TABLE "kolab_cache_contact" DROP COLUMN "tags";
6
+ALTER TABLE "kolab_cache_contact" RENAME COLUMN "tags1" TO "tags";
7
+
8
+ALTER TABLE "kolab_cache_event" ADD "tags1" clob DEFAULT NULL;
9
+UPDATE "kolab_cache_event" SET "tags1" = "tags";
10
+ALTER TABLE "kolab_cache_event" DROP COLUMN "tags";
11
+ALTER TABLE "kolab_cache_event" RENAME COLUMN "tags1" TO "tags";
12
+
13
+ALTER TABLE "kolab_cache_task" ADD "tags1" clob DEFAULT NULL;
14
+UPDATE "kolab_cache_task" SET "tags1" = "tags";
15
+ALTER TABLE "kolab_cache_task" DROP COLUMN "tags";
16
+ALTER TABLE "kolab_cache_task" RENAME COLUMN "tags1" TO "tags";
17
+
18
+ALTER TABLE "kolab_cache_journal" ADD "tags1" clob DEFAULT NULL;
19
+UPDATE "kolab_cache_journal" SET "tags1" = "tags";
20
+ALTER TABLE "kolab_cache_journal" DROP COLUMN "tags";
21
+ALTER TABLE "kolab_cache_journal" RENAME COLUMN "tags1" TO "tags";
22
+
23
+ALTER TABLE "kolab_cache_note" ADD "tags1" clob DEFAULT NULL;
24
+UPDATE "kolab_cache_note" SET "tags1" = "tags";
25
+ALTER TABLE "kolab_cache_note" DROP COLUMN "tags";
26
+ALTER TABLE "kolab_cache_note" RENAME COLUMN "tags1" TO "tags";
27
+
28
+ALTER TABLE "kolab_cache_file" ADD "tags1" clob DEFAULT NULL;
29
+UPDATE "kolab_cache_file" SET "tags1" = "tags";
30
+ALTER TABLE "kolab_cache_file" DROP COLUMN "tags";
31
+ALTER TABLE "kolab_cache_file" RENAME COLUMN "tags1" TO "tags";
32
+
33
+ALTER TABLE "kolab_cache_configuration" ADD "tags1" clob DEFAULT NULL;
34
+UPDATE "kolab_cache_configuration" SET "tags1" = "tags";
35
+ALTER TABLE "kolab_cache_configuration" DROP COLUMN "tags";
36
+ALTER TABLE "kolab_cache_configuration" RENAME COLUMN "tags1" TO "tags";
37
+
38
+ALTER TABLE "kolab_cache_freebusy" ADD "tags1" clob DEFAULT NULL;
39
+UPDATE "kolab_cache_freebusy" SET "tags1" = "tags";
40
+ALTER TABLE "kolab_cache_freebusy" DROP COLUMN "tags";
41
+ALTER TABLE "kolab_cache_freebusy" RENAME COLUMN "tags1" TO "tags";
42
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/SQL/postgres.initial.sql Added
33
 
1
@@ -0,0 +1,31 @@
2
+/**
3
+ * libkolab database schema
4
+ *
5
+ * @version @package_version@
6
+ * @author Sidlyarenko Sergey
7
+ * @licence GNU AGPL
8
+ **/
9
+
10
+DROP TABLE IF EXISTS kolab_cache;
11
+
12
+CREATE TABLE kolab_cache (
13
+  resource character varying(255) NOT NULL,
14
+  type character varying(32) NOT NULL,
15
+  msguid NUMERIC(20) NOT NULL,
16
+  uid character varying(128) NOT NULL,
17
+  created timestamp without time zone DEFAULT NULL,
18
+  changed timestamp without time zone DEFAULT NULL,
19
+  data text NOT NULL,
20
+  xml text NOT NULL,
21
+  dtstart timestamp without time zone,
22
+  dtend timestamp without time zone,
23
+  tags character varying(255) NOT NULL,
24
+  words text NOT NULL,
25
+  filename character varying(255) DEFAULT NULL,
26
+  PRIMARY KEY(resource, type, msguid)
27
+);
28
+
29
+CREATE INDEX kolab_cache_resource_filename_idx ON kolab_cache (resource, filename);
30
+
31
+
32
+INSERT INTO system (name, value) VALUES ('libkolab-version', '2013041900');
33
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/UPGRADING Added
11
 
1
@@ -0,0 +1,9 @@
2
+UPGRADING instructions
3
+======================
4
+
5
+To update database schema please run in Roundcube bin/ directory:
6
+
7
+updatedb.sh --package=libkolab --version=<version> --dir=../plugins/libkolab/SQL
8
+
9
+[*] Replace <version> with Roundcube version e.g. 0.7.3
10
+[*] Roundcube should be upgraded before plugin upgrades
11
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/bin Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/bin/modcache.sh Added
237
 
1
@@ -0,0 +1,235 @@
2
+#!/usr/bin/env php
3
+<?php
4
+
5
+/**
6
+ * Kolab storage cache modification script
7
+ *
8
+ * @version 3.1
9
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
10
+ *
11
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
12
+ *
13
+ * This program is free software: you can redistribute it and/or modify
14
+ * it under the terms of the GNU Affero General Public License as
15
+ * published by the Free Software Foundation, either version 3 of the
16
+ * License, or (at your option) any later version.
17
+ *
18
+ * This program is distributed in the hope that it will be useful,
19
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
+ * GNU Affero General Public License for more details.
22
+ *
23
+ * You should have received a copy of the GNU Affero General Public License
24
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
25
+ */
26
+
27
+define('INSTALL_PATH', realpath('.') . '/' );
28
+ini_set('display_errors', 1);
29
+
30
+if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
31
+    die("Execute this from the Roundcube installation dir!\n\n");
32
+
33
+require_once INSTALL_PATH . 'program/include/clisetup.php';
34
+
35
+function print_usage()
36
+{
37
+   print "Usage:  modcache.sh [OPTIONS] ACTION [USERNAME ARGS ...]\n";
38
+   print "Possible actions are: expunge, clear, prewarm\n";
39
+   print "-a, --all      Clear/expunge all caches\n";
40
+   print "-h, --host     IMAP host name\n";
41
+   print "-u, --user     IMAP user name to authenticate\n";
42
+   print "-t, --type     Object types to clear/expunge cache\n";
43
+   print "-l, --limit    Limit the number of records to be expunged\n";
44
+}
45
+
46
+// read arguments
47
+$opts = get_opt(array(
48
+    'a' => 'all',
49
+    'h' => 'host',
50
+    'u' => 'user',
51
+    'p' => 'password',
52
+    't' => 'type',
53
+    'l' => 'limit',
54
+    'v' => 'verbose',
55
+));
56
+
57
+$opts['username'] = !empty($opts[1]) ? $opts[1] : $opts['user'];
58
+$action = $opts[0];
59
+
60
+$rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
61
+
62
+
63
+// connect to database
64
+$db = $rcmail->get_dbh();
65
+$db->db_connect('w');
66
+if (!$db->is_connected() || $db->is_error())
67
+    die("No DB connection\n");
68
+
69
+ini_set('display_errors', 1);
70
+
71
+/*
72
+ * Script controller
73
+ */
74
+switch (strtolower($action)) {
75
+
76
+/*
77
+ * Clear/expunge all cache records
78
+ */
79
+case 'expunge':
80
+    $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','journal','note','task');
81
+    $folder_types_db = array_map(array($db, 'quote'), $folder_types);
82
+    $expire = strtotime(!empty($opts[2]) ? $opts[2] : 'now - 10 days');
83
+    $sql_where = "type IN (" . join(',', $folder_types_db) . ")";
84
+
85
+    if ($opts['username']) {
86
+        $sql_where .= ' AND resource LIKE ?';
87
+    }
88
+
89
+    $sql_query = "DELETE FROM %s WHERE folder_id IN (SELECT folder_id FROM kolab_folders WHERE $sql_where) AND created <= " . $db->quote(date('Y-m-d 00:00:00', $expire));
90
+    if ($opts['limit']) {
91
+        $sql_query = ' LIMIT ' . intval($opts['limit']);
92
+    }
93
+    foreach ($folder_types as $type) {
94
+        $table_name = 'kolab_cache_' . $type;
95
+        $db->query(sprintf($sql_query, $table_name), resource_prefix($opts).'%');
96
+        echo $db->affected_rows() . " records deleted from '$table_name'\n";
97
+    }
98
+
99
+    $db->query("UPDATE kolab_folders SET ctag='' WHERE $sql_where", resource_prefix($opts).'%');
100
+    break;
101
+
102
+case 'clear':
103
+    $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','journal','note','task');
104
+    $folder_types_db = array_map(array($db, 'quote'), $folder_types);
105
+
106
+    if ($opts['all']) {
107
+        $sql_query = "DELETE FROM kolab_folders WHERE 1";
108
+    }
109
+    else if ($opts['username']) {
110
+        $sql_query = "DELETE FROM kolab_folders WHERE type IN (" . join(',', $folder_types_db) . ") AND resource LIKE ?";
111
+    }
112
+
113
+    if ($sql_query) {
114
+        $db->query($sql_query, resource_prefix($opts).'%');
115
+        echo $db->affected_rows() . " records deleted from 'kolab_folders'\n";
116
+    }
117
+    break;
118
+
119
+
120
+/*
121
+ * Prewarm cache by synchronizing objects for the given user
122
+ */
123
+case 'prewarm':
124
+    // make sure libkolab classes are loaded
125
+    $rcmail->plugins->load_plugin('libkolab');
126
+
127
+    if (authenticate($opts)) {
128
+        $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','task');
129
+        foreach ($folder_types as $type) {
130
+            // sync every folder of the given type
131
+            foreach (kolab_storage::get_folders($type) as $folder) {
132
+                echo "Synching " . $folder->name . " ($type) ... ";
133
+                echo $folder->count($type) . "\n";
134
+
135
+                // also sync distribution lists in contact folders
136
+                if ($type == 'contact') {
137
+                    echo "Synching " . $folder->name . " (distribution-list) ... ";
138
+                    echo $folder->count('distribution-list') . "\n";
139
+                }
140
+            }
141
+        }
142
+    }
143
+    else
144
+        die("Authentication failed for " . $opts['user']);
145
+    break;
146
+
147
+/**
148
+ * Update the cache meta columns from the serialized/xml data
149
+ * (might be run after a schema update)
150
+ */
151
+case 'update':
152
+    // make sure libkolab classes are loaded
153
+    $rcmail->plugins->load_plugin('libkolab');
154
+
155
+    $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','task');
156
+    foreach ($folder_types as $type) {
157
+        $class = 'kolab_storage_cache_' . $type;
158
+        $sql_result = $db->query("SELECT folder_id FROM kolab_folders WHERE type=? AND synclock = 0", $type);
159
+        while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
160
+            $folder = new $class;
161
+            $folder->select_by_id($sql_arr['folder_id']);
162
+            echo "Updating " . $sql_arr['folder_id'] . " ($type) ";
163
+            foreach ($folder->select() as $object) {
164
+                $object['_formatobj']->to_array();  // load data
165
+                $folder->save($object['_msguid'], $object, $object['_msguid']);
166
+                echo ".";
167
+            }
168
+            echo "done.\n";
169
+        }
170
+    }
171
+    break;
172
+
173
+
174
+/*
175
+ * Unknown action => show usage
176
+ */
177
+default:
178
+    print_usage();
179
+    exit;
180
+}
181
+
182
+
183
+/**
184
+ * Compose cache resource URI prefix for the given user credentials
185
+ */
186
+function resource_prefix($opts)
187
+{
188
+    return 'imap://' . str_replace('%', '\\%', urlencode($opts['username'])) . '@' . $opts['host'] . '/';
189
+}
190
+
191
+
192
+/**
193
+ * Authenticate to the IMAP server with the given user credentials
194
+ */
195
+function authenticate(&$opts)
196
+{
197
+    global $rcmail;
198
+
199
+    // prompt for password
200
+    if (empty($opts['password']) && ($opts['username'] || $opts['user'])) {
201
+        $opts['password'] = prompt_silent("Password: ");
202
+    }
203
+
204
+    // simulate "login as" feature
205
+    if ($opts['user'] && $opts['user'] != $opts['username'])
206
+        $_POST['_loginas'] = $opts['username'];
207
+    else if (empty($opts['user']))
208
+        $opts['user'] = $opts['username'];
209
+
210
+    // let the kolab_auth plugin do its magic
211
+    $auth = $rcmail->plugins->exec_hook('authenticate', array(
212
+        'host' => trim($opts['host']),
213
+        'user' => trim($opts['user']),
214
+        'pass' => $opts['password'],
215
+        'cookiecheck' => false,
216
+        'valid' => !empty($opts['user']) && !empty($opts['host']),
217
+    ));
218
+
219
+    if ($auth['valid']) {
220
+        $storage = $rcmail->get_storage();
221
+        if ($storage->connect($auth['host'], $auth['user'], $auth['pass'], 143, false)) {
222
+            if ($opts['verbose'])
223
+                echo "IMAP login succeeded.\n";
224
+            if (($user = rcube_user::query($opts['username'], $auth['host'])) && $user->ID)
225
+                $rcmail->user = $user;
226
+        }
227
+        else
228
+            die("Login to IMAP server failed!\n");
229
+    }
230
+    else {
231
+        die("Invalid login credentials!\n");
232
+    }
233
+
234
+    return $auth['valid'];
235
+}
236
+
237
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/bin/randomcontacts.sh Added
183
 
1
@@ -0,0 +1,181 @@
2
+#!/usr/bin/env php
3
+<?php
4
+
5
+/**
6
+ * Generate a number contacts with random data
7
+ *
8
+ * @version 3.1
9
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
10
+ *
11
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
12
+ *
13
+ * This program is free software: you can redistribute it and/or modify
14
+ * it under the terms of the GNU Affero General Public License as
15
+ * published by the Free Software Foundation, either version 3 of the
16
+ * License, or (at your option) any later version.
17
+ *
18
+ * This program is distributed in the hope that it will be useful,
19
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
+ * GNU Affero General Public License for more details.
22
+ *
23
+ * You should have received a copy of the GNU Affero General Public License
24
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
25
+ */
26
+
27
+define('INSTALL_PATH', realpath('.') . '/' );
28
+ini_set('display_errors', 1);
29
+
30
+if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
31
+    die("Execute this from the Roundcube installation dir!\n\n");
32
+
33
+require_once INSTALL_PATH . 'program/include/clisetup.php';
34
+
35
+function print_usage()
36
+{
37
+    print "Usage:  randomcontacts.sh [OPTIONS] USERNAME FOLDER\n";
38
+    print "Create random contact that for then given user in the specified folder.\n";
39
+    print "-n, --num      Number of contacts to be created, defaults to 50\n";
40
+    print "-h, --host     IMAP host name\n";
41
+    print "-p, --password IMAP user password\n";
42
+}
43
+
44
+// read arguments
45
+$opts = get_opt(array(
46
+    'n' => 'num',
47
+    'h' => 'host',
48
+    'u' => 'user',
49
+    'p' => 'pass',
50
+    'v' => 'verbose',
51
+));
52
+
53
+$opts['username'] = !empty($opts[0]) ? $opts[0] : $opts['user'];
54
+$opts['folder'] = $opts[1];
55
+
56
+$rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
57
+$rcmail->plugins->load_plugins(array('libkolab'));
58
+ini_set('display_errors', 1);
59
+
60
+
61
+if (empty($opts['host'])) {
62
+    $opts['host'] = $rcmail->config->get('default_host');
63
+    if (is_array($opts['host']))  // not unique
64
+        $opts['host'] = null;
65
+}
66
+
67
+if (empty($opts['username']) || empty($opts['folder']) || empty($opts['host'])) {
68
+    print_usage();
69
+    exit;
70
+}
71
+
72
+// prompt for password
73
+if (empty($opts['pass'])) {
74
+    $opts['pass'] = rcube_utils::prompt_silent("Password: ");
75
+}
76
+
77
+// parse $host URL
78
+$a_host = parse_url($opts['host']);
79
+if ($a_host['host']) {
80
+    $host = $a_host['host'];
81
+    $imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? TRUE : FALSE;
82
+    $imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
83
+}
84
+else {
85
+    $host = $opts['host'];
86
+    $imap_port = 143;
87
+}
88
+
89
+// instantiate IMAP class
90
+$IMAP = $rcmail->get_storage();
91
+
92
+// try to connect to IMAP server
93
+if ($IMAP->connect($host, $opts['username'], $opts['pass'], $imap_port, $imap_ssl)) {
94
+    print "IMAP login successful.\n";
95
+    $user = rcube_user::query($opts['username'], $host);
96
+    $rcmail->user = $user ?: new rcube_user(null, array('username' => $opts['username'], 'host' => $host));
97
+}
98
+else {
99
+    die("IMAP login failed for user " . $opts['username'] . " @ $host\n");
100
+}
101
+
102
+// get contacts folder
103
+$folder = kolab_storage::get_folder($opts['folder']);
104
+if (!$folder || empty($folder->type)) {
105
+    die("Invalid Address Book " . $opts['folder'] . "\n");
106
+}
107
+
108
+$format = new kolab_format_contact;
109
+
110
+$num = $opts['num'] ? intval($opts['num']) : 50;
111
+echo "Creating $num contacts in " . $folder->get_resource_uri() . "\n";
112
+
113
+for ($i=0; $i < $num; $i++) {
114
+    // generate random names
115
+    $contact = array(
116
+        'surname' => random_string(rand(1,2)),
117
+        'firstname' => random_string(rand(1,2)),
118
+        'organization' => random_string(rand(0,2)),
119
+        'profession' => random_string(rand(1,2)),
120
+        'email' => array(),
121
+        'phone' => array(),
122
+        'address' => array(),
123
+        'notes' => random_string(rand(10,200)),
124
+    );
125
+
126
+    // randomly add email addresses
127
+    $em = rand(1,3);
128
+    for ($e=0; $e < $em; $e++) {
129
+        $type = array_rand($format->emailtypes);
130
+        $contact['email'][] = array(
131
+            'address' => strtolower(random_string(1) . '@' . random_string(1) . '.tld'),
132
+            'type' => $type,
133
+        );
134
+    }
135
+
136
+    // randomly add phone numbers
137
+    $ph = rand(1,4);
138
+    for ($p=0; $p < $ph; $p++) {
139
+        $type = array_rand($format->phonetypes);
140
+        $contact['phone'][] = array(
141
+            'number' => '+'.rand(2,8).rand(1,9).rand(1,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9),
142
+            'type' => $type,
143
+        );
144
+    }
145
+
146
+    // randomly add addresses
147
+    $ad = rand(0,2);
148
+    for ($a=0; $a < $ad; $a++) {
149
+        $type = array_rand($format->addresstypes);
150
+        $contact['address'][] = array(
151
+            'street' => random_string(rand(1,3)),
152
+            'locality' => random_string(rand(1,2)),
153
+            'code' => rand(1000, 89999),
154
+            'country' => random_string(1),
155
+            'type' => $type,
156
+        );
157
+    }
158
+
159
+    $contact['name'] = $contact['firstname'] . ' ' . $contact['surname'];
160
+
161
+    if ($folder->save($contact, 'contact')) {
162
+        echo ".";
163
+    }
164
+    else {
165
+        echo "x";
166
+        break;  // abort on error
167
+    }
168
+}
169
+
170
+echo " done.\n";
171
+
172
+
173
+
174
+function random_string($len)
175
+{
176
+    $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a features is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform hereafter referred to without the classical prefix retains many applications, as most manufac- tured parts and many anatomical parts investigated in medical imagery contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise.");
177
+    for ($i = 0; $i < $len; $i++) {
178
+        $str .= $words[rand(0,count($words)-1)] . " ";
179
+    }
180
+
181
+    return rtrim($str);
182
+}
183
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/bin/readcache.sh Added
152
 
1
@@ -0,0 +1,150 @@
2
+#!/usr/bin/env php
3
+<?php
4
+
5
+/**
6
+ * Kolab storage cache testing script
7
+ *
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+
26
+define('INSTALL_PATH', realpath('.') . '/' );
27
+ini_set('display_errors', 1);
28
+libxml_use_internal_errors(true);
29
+
30
+if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
31
+    die("Execute this from the Roundcube installation dir!\n\n");
32
+
33
+require_once INSTALL_PATH . 'program/include/clisetup.php';
34
+
35
+function print_usage()
36
+{
37
+   print "Usage:  readcache.sh [OPTIONS] FOLDER\n";
38
+   print "-h, --host     IMAP host name\n";
39
+   print "-l, --limit    Limit the number of records to be listed\n";
40
+}
41
+
42
+// read arguments
43
+$opts = get_opt(array(
44
+    'h' => 'host',
45
+    'l' => 'limit',
46
+    'v' => 'verbose',
47
+));
48
+
49
+$folder = $opts[0];
50
+$imap_host = $opts['host'];
51
+
52
+$rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
53
+
54
+if (empty($imap_host)) {
55
+    $default_host = $rcmail->config->get('default_host');
56
+    if (is_array($default_host)) {
57
+        list($k,$v) = each($default_host);
58
+        $imap_host = is_numeric($k) ? $v : $k;
59
+    }
60
+    else {
61
+        $imap_host = $default_host;
62
+    }
63
+
64
+    // strip protocol prefix
65
+    $imap_host = preg_replace('!^[a-z]+://!', '', $imap_host);
66
+}
67
+
68
+if (empty($folder) || empty($imap_host)) {
69
+    print_usage();
70
+    exit;
71
+}
72
+
73
+// connect to database
74
+$db = $rcmail->get_dbh();
75
+$db->db_connect('r');
76
+if (!$db->is_connected() || $db->is_error())
77
+    die("No DB connection\n");
78
+
79
+
80
+// resolve folder_id
81
+if (!is_numeric($folder)) {
82
+    if (strpos($folder, '@')) {
83
+        list($mailbox, $domain) = explode('@', $folder);
84
+        list($username, $subpath) = explode('/', preg_replace('!^user/!', '', $mailbox), 2);
85
+        $folder_uri = 'imap://' . urlencode($username.'@'.$domain) . '@' . $imap_host . '/' . $subpath;
86
+    }
87
+    else {
88
+        die("Invalid mailbox identifier! Example: user/john.doe/Calendar@example.org\n");
89
+    }
90
+
91
+    print "Resolving folder $folder_uri...";
92
+    $sql_result = $db->query('SELECT * FROM `kolab_folders` WHERE `resource`=?', $folder_uri);
93
+    if ($sql_result && ($folder_data = $db->fetch_assoc($sql_result))) {
94
+        $folder_id = $folder_data['folder_id'];
95
+        print $folder_id;
96
+    }
97
+    print "\n";
98
+}
99
+else {
100
+    $folder_id = intval($folder);
101
+    $sql_result = $db->query('SELECT * FROM `kolab_folders` WHERE `folder_id`=?', $folder_id);
102
+    if ($sql_result) {
103
+        $folder_data = $db->fetch_assoc($sql_result);
104
+    }
105
+}
106
+
107
+if (empty($folder_data)) {
108
+    die("Can't find cache mailbox for '$folder'\n");
109
+}
110
+
111
+print "Querying cache for folder $folder_id ($folder_data[type])...\n";
112
+
113
+$extra_cols = array(
114
+    'event'   => array('dtstart','dtend'),
115
+    'contact' => array('type'),
116
+);
117
+
118
+$cache_table = $db->table_name('kolab_cache_' . $folder_data['type']);
119
+$extra_cols_ = $extra_cols[$folder_data['type']] ?: array();
120
+$sql_arr = $db->fetch_assoc($db->query("SELECT COUNT(*) as cnt FROM `$cache_table` WHERE `folder_id`=?", intval($folder_id)));
121
+
122
+print "CTag  = " . $folder_data['ctag'] . "\n";
123
+print "Lock  = " . $folder_data['synclock'] . "\n";
124
+print "Count = " . $sql_arr['cnt'] . "\n";
125
+print "----------------------------------------------------------------------------------\n";
126
+print "<MSG>\t<UUID>\t<CHANGED>\t<DATA>\t<XML>\t";
127
+print join("\t", array_map(function($c) { return '<' . strtoupper($c) . '>'; }, $extra_cols_));
128
+print "\n----------------------------------------------------------------------------------\n";
129
+
130
+$result = $db->limitquery("SELECT * FROM `$cache_table` WHERE `folder_id`=?", 0, $opts['limit'], intval($folder_id));
131
+while ($result && ($sql_arr = $db->fetch_assoc($result))) {
132
+    print $sql_arr['msguid'] . "\t" . $sql_arr['uid'] . "\t" . $sql_arr['changed'];
133
+
134
+    // try to unserialize data block
135
+    $object = @unserialize(@base64_decode($sql_arr['data']));
136
+    print "\t" . ($object === false ? 'FAIL!' : ($object['uid'] == $sql_arr['uid'] ? 'OK' : '!!!'));
137
+
138
+    // check XML validity
139
+    $xml = simplexml_load_string($sql_arr['xml']);
140
+    print "\t" . ($xml === false ? 'FAIL!' : 'OK');
141
+
142
+    // print extra cols
143
+    array_walk($extra_cols_, function($c) use ($sql_arr) {
144
+        print "\t" . $sql_arr[$c];
145
+    });
146
+
147
+    print "\n";
148
+}
149
+
150
+print "----------------------------------------------------------------------------------\n";
151
+echo "Done.\n";
152
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/composer.json Added
32
 
1
@@ -0,0 +1,30 @@
2
+{
3
+    "name": "kolab/libkolab",
4
+    "type": "roundcube-plugin",
5
+    "description": "Plugin to setup a basic environment for the interaction with a Kolab server.",
6
+    "homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
7
+    "license": "AGPLv3",
8
+    "version": "3.2.3",
9
+    "authors": [
10
+        {
11
+            "name": "Thomas Bruederli",
12
+            "email": "bruederli@kolabsys.com",
13
+            "role": "Lead"
14
+        },
15
+        {
16
+            "name": "Alensader Machniak",
17
+            "email": "machniak@kolabsys.com",
18
+            "role": "Developer"
19
+        }
20
+    ],
21
+    "repositories": [
22
+        {
23
+            "type": "composer",
24
+            "url": "http://plugins.roundcube.net"
25
+        }
26
+    ],
27
+    "require": {
28
+        "php": ">=5.3.0",
29
+        "roundcube/plugin-installer": ">=0.1.3"
30
+    }
31
+}
32
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/config.inc.php.dist Added
64
 
1
@@ -0,0 +1,62 @@
2
+<?php
3
+
4
+/* Configuration for libkolab */
5
+
6
+// Enable caching of Kolab objects in local database
7
+$config['kolab_cache'] = true;
8
+
9
+// Specify format version to write Kolab objects (must be a string value!)
10
+$config['kolab_format_version']  = '3.0';
11
+
12
+// Optional override of the URL to read and trigger Free/Busy information of Kolab users
13
+// Defaults to https://<imap-server->/freebusy
14
+$config['kolab_freebusy_server'] = null;
15
+
16
+// Enables listing of only subscribed folders. This e.g. will limit
17
+// folders in calendar view or available addressbooks
18
+$config['kolab_use_subscriptions'] = false;
19
+
20
+// List any of 'personal','shared','other' namespaces to be excluded from groupware folder listing
21
+// example: array('other');
22
+$config['kolab_skip_namespace'] = null;
23
+
24
+// Enables the use of displayname folder annotations as introduced in KEP:?
25
+// for displaying resource folder names (experimental!)
26
+$config['kolab_custom_display_names'] = false;
27
+
28
+// Configuration of HTTP requests.
29
+// See http://pear.php.net/manual/en/package.http.http-request2.config.php
30
+// for list of supported configuration options (array keys)
31
+$config['kolab_http_request'] = array();
32
+
33
+// When kolab_cache is enabled Roundcube's messages cache will be redundant
34
+// when working on kolab folders. Here we can:
35
+// 2 - bypass messages/indexes cache completely
36
+// 1 - bypass only messages, but use index cache
37
+$config['kolab_messages_cache_bypass'] = 0;
38
+
39
+// LDAP directory to find avilable users for folder sharing.
40
+// Either contains an array with LDAP addressbook configuration or refers to entry in $config['ldap_public'].
41
+// If not specified, the configuraton from 'kolab_auth_addressbook' will be used.
42
+// Should be provided for multi-domain setups with placeholders like %dc, %d, %u, %fu or %dn.
43
+$config['kolab_users_directory'] = null;
44
+
45
+// Filter to be used for resolving user folders in LDAP.
46
+// Defaults to the 'kolab_auth_filter' configuration option.
47
+$config['kolab_users_filter'] = '(&(objectclass=kolabInetOrgPerson)(|(uid=%u)(mail=%fu)))';
48
+
49
+// Which property of the LDAP user record to use for user folder mapping in IMAP.
50
+// Defaults to the 'kolab_auth_login' configuration option.
51
+$config['kolab_users_id_attrib'] = null;
52
+
53
+// Use these attributes when searching users in LDAP
54
+$config['kolab_users_search_attrib'] = array('cn','mail','alias');
55
+
56
+// JSON-RPC endpoint configuration of the Bonnie web service providing historic data for groupware objects
57
+$config['kolab_bonnie_api'] = array(
58
+    'uri'    => 'https://<kolab-hostname>:8080/api/rpc',
59
+    'user'   => 'webclient',
60
+    'pass'   => 'Welcome2KolabSystems',
61
+    'secret' => '8431f191707fffffff00000000cccc',
62
+    'debug'  => true,   // logs requests/responses to <log-dir>/bonnie
63
+);
64
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/js Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/js/folderlist.js Added
352
 
1
@@ -0,0 +1,350 @@
2
+/**
3
+ * Kolab groupware folders treelist widget
4
+ *
5
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
6
+ *
7
+ * @licstart  The following is the entire license notice for the
8
+ * JavaScript code in this file.
9
+ *
10
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ *
25
+ * @licend  The above is the entire license notice
26
+ * for the JavaScript code in this file.
27
+ */
28
+
29
+function kolab_folderlist(node, p)
30
+{
31
+    // extends treelist.js
32
+    rcube_treelist_widget.call(this, node, p);
33
+
34
+    // private vars
35
+    var me = this;
36
+    var search_results;
37
+    var search_results_widget;
38
+    var search_results_container;
39
+    var listsearch_request;
40
+    var search_messagebox;
41
+
42
+    var Q = rcmail.quote_html;
43
+
44
+    // render the results for folderlist search
45
+    function render_search_results(results)
46
+    {
47
+        if (results.length) {
48
+          // create treelist widget to present the search results
49
+          if (!search_results_widget) {
50
+              var list_id = (me.container.attr('id') || p.id_prefix || '0')
51
+              search_results_container = $('<div class="searchresults"></div>')
52
+                  .html(p.search_title ? '<h2 class="boxtitle" id="st:' + list_id + '">' + p.search_title + '</h2>' : '')
53
+                  .insertAfter(me.container);
54
+
55
+              search_results_widget = new rcube_treelist_widget('<ul>', {
56
+                  id_prefix: p.id_prefix,
57
+                  id_encode: p.id_encode,
58
+                  id_decode: p.id_decode,
59
+                  selectable: false
60
+              });
61
+              // copy classes from main list
62
+              search_results_widget.container.addClass(me.container.attr('class')).attr('aria-labelledby', 'st:' + list_id);
63
+
64
+              // register click handler on search result's checkboxes to select the given item for listing
65
+              search_results_widget.container
66
+                  .appendTo(search_results_container)
67
+                  .on('click', 'input[type=checkbox], a.subscribed, span.subscribed', function(e) {
68
+                      var node, has_children, li = $(this).closest('li'),
69
+                          id = li.attr('id').replace(new RegExp('^'+p.id_prefix), '');
70
+                      if (p.id_decode)
71
+                          id = p.id_decode(id);
72
+                      node = search_results_widget.get_node(id);
73
+                      has_children = node.children && node.children.length;
74
+
75
+                      e.stopPropagation();
76
+                      e.bubbles = false;
77
+
78
+                      // activate + subscribe
79
+                      if ($(e.target).hasClass('subscribed')) {
80
+                          search_results[id].subscribed = true;
81
+                          $(e.target).attr('aria-checked', 'true');
82
+                          li.children().first()
83
+                              .toggleClass('subscribed')
84
+                              .find('input[type=checkbox]').get(0).checked = true;
85
+
86
+                          if (has_children && search_results[id].group == 'other user') {
87
+                              li.find('ul li > div').addClass('subscribed')
88
+                                  .find('a.subscribed').attr('aria-checked', 'true');;
89
+                          }
90
+                      }
91
+                      else if (!this.checked) {
92
+                          return;
93
+                      }
94
+
95
+                      // copy item to the main list
96
+                      add_result2list(id, li, true);
97
+
98
+                      if (has_children) {
99
+                          li.find('input[type=checkbox]').first().prop('disabled', true).prop('checked', true);
100
+                          li.find('a.subscribed, span.subscribed').first().hide();
101
+                      }
102
+                      else {
103
+                          li.remove();
104
+                      }
105
+
106
+                      // set partial subscription status
107
+                      if (search_results[id].subscribed && search_results[id].parent && search_results[id].group == 'other') {
108
+                          parent_subscription_status($(me.get_item(id, true)));
109
+                      }
110
+
111
+                      // set focus to cloned checkbox
112
+                      if (rcube_event.is_keyboard(e)) {
113
+                        $(me.get_item(id, true)).find('input[type=checkbox]').first().focus();
114
+                      }
115
+                  })
116
+                  .on('click', function(e) {
117
+                      var prop, id = String($(e.target).closest('li').attr('id')).replace(new RegExp('^'+p.id_prefix), '');
118
+                      if (p.id_decode)
119
+                          id = p.id_decode(id);
120
+
121
+                      // forward event
122
+                      if (prop = search_results[id]) {
123
+                        e.data = prop;
124
+                        if (me.triggerEvent('click-item', e) === false) {
125
+                          e.stopPropagation();
126
+                          return false;
127
+                        }
128
+                      }
129
+                  });
130
+          }
131
+
132
+          // add results to list
133
+          for (var prop, item, i=0; i < results.length; i++) {
134
+              prop = results[i];
135
+              item = $(prop.html);
136
+              search_results[prop.id] = prop;
137
+              search_results_widget.insert({
138
+                  id: prop.id,
139
+                  classes: [ prop.group || '' ],
140
+                  html: item,
141
+                  collapsed: true,
142
+                  virtual: prop.virtual
143
+              }, prop.parent);
144
+
145
+              // disable checkbox if item already exists in main list
146
+              if (me.get_node(prop.id) && !me.get_node(prop.id).virtual) {
147
+                  item.find('input[type=checkbox]').first().prop('disabled', true).prop('checked', true);
148
+                  item.find('a.subscribed, span.subscribed').hide();
149
+              }
150
+          }
151
+
152
+          search_results_container.show();
153
+        }
154
+    }
155
+
156
+    // helper method to (recursively) add a search result item to the main list widget
157
+    function add_result2list(id, li, active)
158
+    {
159
+        var node = search_results_widget.get_node(id),
160
+            prop = search_results[id],
161
+            parent_id = prop.parent || null,
162
+            has_children = node.children && node.children.length,
163
+            dom_node = has_children ? li.children().first().clone(true, true) : li.children().first(),
164
+            childs = [];
165
+
166
+        // find parent node and insert at the right place
167
+        if (parent_id && me.get_node(parent_id)) {
168
+            dom_node.children('span,a').first().html(Q(prop.editname || prop.listname));
169
+        }
170
+        else if (parent_id && search_results[parent_id]) {
171
+            // copy parent tree from search results
172
+            add_result2list(parent_id, $(search_results_widget.get_item(parent_id)), false);
173
+        }
174
+        else if (parent_id) {
175
+            // use full name for list display
176
+            dom_node.children('span,a').first().html(Q(prop.name));
177
+        }
178
+
179
+        // replace virtual node with a real one
180
+        if (me.get_node(id)) {
181
+            $(me.get_item(id, true)).children().first()
182
+                .replaceWith(dom_node)
183
+                .removeClass('virtual');
184
+        }
185
+        else {
186
+            // copy childs, too
187
+            if (has_children && prop.group == 'other user') {
188
+                for (var cid, j=0; j < node.children.length; j++) {
189
+                    if ((cid = node.children[j].id) && search_results[cid]) {
190
+                        childs.push(search_results_widget.get_node(cid));
191
+                    }
192
+                }
193
+            }
194
+
195
+            // move this result item to the main list widget
196
+            me.insert({
197
+                id: id,
198
+                classes: [ prop.group || '' ],
199
+                virtual: prop.virtual,
200
+                html: dom_node,
201
+                level: node.level,
202
+                collapsed: true,
203
+                children: childs
204
+            }, parent_id, prop.group);
205
+        }
206
+
207
+        delete prop.html;
208
+        prop.active = active;
209
+        me.triggerEvent('insert-item', { id: id, data: prop, item: li });
210
+
211
+        // register childs, too
212
+        if (childs.length) {
213
+            for (var cid, j=0; j < node.children.length; j++) {
214
+                if ((cid = node.children[j].id) && search_results[cid]) {
215
+                    prop = search_results[cid];
216
+                    delete prop.html;
217
+                    prop.active = false;
218
+                    me.triggerEvent('insert-item', { id: cid, data: prop });
219
+                }
220
+            }
221
+        }
222
+    }
223
+
224
+    // update the given item's parent's (partial) subscription state
225
+    function parent_subscription_status(li)
226
+    {
227
+        var top_li = li.closest(me.container.children('li')),
228
+            all_childs = $('li > div:not(.treetoggle)', top_li),
229
+            subscribed = all_childs.filter('.subscribed').length;
230
+
231
+        if (subscribed == 0) {
232
+            top_li.children('div:first').removeClass('subscribed partial');
233
+        }
234
+        else {
235
+            top_li.children('div:first')
236
+                .addClass('subscribed')[subscribed < all_childs.length ? 'addClass' : 'removeClass']('partial');
237
+        }
238
+    }
239
+
240
+    // do some magic when search is performed on the widget
241
+    this.addEventListener('search', function(search) {
242
+        // hide search results
243
+        if (search_results_widget) {
244
+            search_results_container.hide();
245
+            search_results_widget.reset();
246
+        }
247
+        search_results = {};
248
+
249
+        if (search_messagebox)
250
+            rcmail.hide_message(search_messagebox);
251
+
252
+        // send search request(s) to server
253
+        if (search.query && search.execute) {
254
+            // require a minimum length for the search string
255
+            if (rcmail.env.autocomplete_min_length && search.query.length < rcmail.env.autocomplete_min_length && search.query != '*') {
256
+                search_messagebox = rcmail.display_message(
257
+                    rcmail.get_label('autocompletechars').replace('$min', rcmail.env.autocomplete_min_length));
258
+                return;
259
+            }
260
+
261
+            if (listsearch_request) {
262
+                // ignore, let the currently running request finish
263
+                if (listsearch_request.query == search.query) {
264
+                    return;
265
+                }
266
+                else { // cancel previous search request
267
+                    rcmail.multi_thread_request_abort(listsearch_request.id);
268
+                    listsearch_request = null;
269
+                }
270
+            }
271
+
272
+            var sources = p.search_sources || [ 'folders' ];
273
+            var reqid = rcmail.multi_thread_http_request({
274
+                items: sources,
275
+                threads: rcmail.env.autocomplete_threads || 1,
276
+                action:  p.search_action || 'listsearch',
277
+                postdata: { action:'search', q:search.query, source:'%s' },
278
+                lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
279
+                onresponse: render_search_results,
280
+                whendone: function(data){
281
+                  listsearch_request = null;
282
+                  me.triggerEvent('search-complete', data);
283
+                }
284
+            });
285
+
286
+            listsearch_request = { id:reqid, query:search.query };
287
+        }
288
+        else if (!search.query && listsearch_request) {
289
+            rcmail.multi_thread_request_abort(listsearch_request.id);
290
+            listsearch_request = null;
291
+        }
292
+    });
293
+
294
+    this.container.on('click', 'a.subscribed, span.subscribed', function(e) {
295
+        var li = $(this).closest('li'),
296
+            id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
297
+            div = li.children().first(),
298
+            is_subscribed;
299
+
300
+        if (me.is_search()) {
301
+            id = id.replace(/--xsR$/, '');
302
+            li = $(me.get_item(id, true));
303
+            div = $(div).add(li.children().first());
304
+        }
305
+
306
+        if (p.id_decode)
307
+            id = p.id_decode(id);
308
+
309
+        div.toggleClass('subscribed');
310
+        is_subscribed = div.hasClass('subscribed');
311
+        $(this).attr('aria-checked', is_subscribed ? 'true' : 'false');
312
+        me.triggerEvent('subscribe', { id: id, subscribed: is_subscribed, item: li });
313
+
314
+        // update subscribe state of all 'virtual user' child folders
315
+        if (li.hasClass('other user')) {
316
+            $('ul li > div', li).each(function() {
317
+                $(this)[is_subscribed ? 'addClass' : 'removeClass']('subscribed');
318
+                $('.subscribed', div).attr('aria-checked', is_subscribed ? 'true' : 'false');
319
+            });
320
+            div.removeClass('partial');
321
+        }
322
+        // propagate subscription state to parent  'virtual user' folder
323
+        else if (li.closest('li.other.user').length) {
324
+            parent_subscription_status(li);
325
+        }
326
+
327
+        e.stopPropagation();
328
+        return false;
329
+    });
330
+
331
+    this.container.on('click', 'a.remove', function(e) {
332
+      var li = $(this).closest('li'),
333
+          id = li.attr('id').replace(new RegExp('^'+p.id_prefix), '');
334
+
335
+      if (me.is_search()) {
336
+          id = id.replace(/--xsR$/, '');
337
+          li = $(me.get_item(id, true));
338
+      }
339
+
340
+      if (p.id_decode)
341
+          id = p.id_decode(id);
342
+
343
+      me.triggerEvent('remove', { id: id, item: li });
344
+
345
+      e.stopPropagation();
346
+      return false;
347
+    });
348
+}
349
+
350
+// link prototype from base class
351
+kolab_folderlist.prototype = rcube_treelist_widget.prototype;
352
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api.php Added
85
 
1
@@ -0,0 +1,82 @@
2
+<?php
3
+
4
+/**
5
+ * Provider class for accessing historic groupware object data through the Bonnie service
6
+ *
7
+ * API Specification at https://wiki.kolabsys.com/User:Bruederli/Draft:Bonnie_Client_API
8
+ *
9
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
10
+ *
11
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
12
+ *
13
+ * This program is free software: you can redistribute it and/or modify
14
+ * it under the terms of the GNU Affero General Public License as
15
+ * published by the Free Software Foundation, either version 3 of the
16
+ * License, or (at your option) any later version.
17
+ *
18
+ * This program is distributed in the hope that it will be useful,
19
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
+ * GNU Affero General Public License for more details.
22
+ *
23
+ * You should have received a copy of the GNU Affero General Public License
24
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
25
+ */
26
+
27
+class kolab_bonnie_api
28
+{
29
+    public $ready = false;
30
+
31
+    private $config = array();
32
+    private $client = null;
33
+
34
+
35
+    /**
36
+     * Default constructor
37
+     */
38
+    public function __construct($config)
39
+    {
40
+        $this->config = $config;
41
+
42
+        $this->client = new kolab_bonnie_api_client($config['uri'], $config['timeout'] ?: 5, (bool)$config['debug']);
43
+
44
+        $this->client->set_secret($config['secret']);
45
+        $this->client->set_authentication($config['user'], $config['pass']);
46
+        $this->client->set_request_user(rcube::get_instance()->get_user_name());
47
+
48
+        $this->ready = !empty($config['secret']) && !empty($config['user']) && !empty($config['pass']);
49
+    }
50
+
51
+    /**
52
+     * Wrapper function for <object>.changelog() API call
53
+     */
54
+    public function changelog($type, $uid, $mailbox=null)
55
+    {
56
+        return $this->client->execute($type.'.changelog', array('uid' => $uid, 'mailbox' => $mailbox));
57
+    }
58
+
59
+    /**
60
+     * Wrapper function for <object>.diff() API call
61
+     */
62
+    public function diff($type, $uid, $rev, $mailbox=null)
63
+    {
64
+        return $this->client->execute($type.'.diff', array('uid' => $uid, 'rev' => $rev, 'mailbox' => $mailbox));
65
+    }
66
+
67
+    /**
68
+     * Wrapper function for <object>.get() API call
69
+     */
70
+    public function get($type, $uid, $rev, $mailbox=null)
71
+    {
72
+      return $this->client->execute($type.'.get', array('uid' => $uid, 'rev' => intval($rev), 'mailbox' => $mailbox));
73
+    }
74
+
75
+    /**
76
+     * Generic wrapper for direct API calls
77
+     */
78
+    public function _execute($method, $params = array())
79
+    {
80
+        return $this->client->execute($method, $params);
81
+    }
82
+
83
+}
84
\ No newline at end of file
85
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api_client.php Added
242
 
1
@@ -0,0 +1,239 @@
2
+<?php
3
+
4
+/**
5
+ * JSON-RPC client class with some extra features for communicating with the Bonnie API service.
6
+ *
7
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
8
+ *
9
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
10
+ *
11
+ * This program is free software: you can redistribute it and/or modify
12
+ * it under the terms of the GNU Affero General Public License as
13
+ * published by the Free Software Foundation, either version 3 of the
14
+ * License, or (at your option) any later version.
15
+ *
16
+ * This program is distributed in the hope that it will be useful,
17
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ * GNU Affero General Public License for more details.
20
+ *
21
+ * You should have received a copy of the GNU Affero General Public License
22
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
+ */
24
+
25
+class kolab_bonnie_api_client
26
+{
27
+    /**
28
+     * URL of the RPC endpoint
29
+     * @var string
30
+     */
31
+    protected $url;
32
+
33
+    /**
34
+     * HTTP client timeout in seconds
35
+     * @var integer
36
+     */
37
+    protected $timeout;
38
+
39
+    /**
40
+     * Debug flag
41
+     * @var bool
42
+     */
43
+    protected $debug;
44
+
45
+    /**
46
+     * Username for authentication
47
+     * @var string
48
+     */
49
+    protected $username;
50
+
51
+    /**
52
+     * Password for authentication
53
+     * @var string
54
+     */
55
+    protected $password;
56
+
57
+    /**
58
+     * Secret key for request signing
59
+     * @var string
60
+     */
61
+    protected $secret;
62
+
63
+    /**
64
+     * Default HTTP headers to send to the server
65
+     * @var array
66
+     */
67
+    protected $headers = array(
68
+        'Connection' => 'close',
69
+        'Content-Type' => 'application/json',
70
+        'Accept' => 'application/json',
71
+    );
72
+
73
+    /**
74
+     * Constructor
75
+     *
76
+     * @param  string  $url      Server URL
77
+     * @param  integer $timeout  Request timeout
78
+     * @param  bool    $debug    Enabled debug logging
79
+     * @param  array   $headers  Custom HTTP headers
80
+     */
81
+    public function __construct($url, $timeout = 5, $debug = false, $headers = array())
82
+    {
83
+        $this->url = $url;
84
+        $this->timeout = $timeout;
85
+        $this->debug = $debug;
86
+        $this->headers = array_merge($this->headers, $headers);
87
+    }
88
+
89
+    /**
90
+     * Setter for secret key for request signing
91
+     */
92
+    public function set_secret($secret)
93
+    {
94
+        $this->secret = $secret;
95
+    }
96
+
97
+    /**
98
+     * Setter for the X-Request-User header
99
+     */
100
+    public function set_request_user($username)
101
+    {
102
+        $this->headers['X-Request-User'] = $username;
103
+    }
104
+
105
+    /**
106
+     * Set authentication parameters
107
+     *
108
+     * @param  string $username  Username
109
+     * @param  string $password  Password
110
+     */
111
+    public function set_authentication($username, $password)
112
+    {
113
+        $this->username = $username;
114
+        $this->password = $password;
115
+    }
116
+
117
+    /**
118
+     * Automatic mapping of procedures
119
+     *
120
+     * @param  string $method  Procedure name
121
+     * @param  array  $params  Procedure arguments
122
+     * @return mixed
123
+     */
124
+    public function __call($method, $params)
125
+    {
126
+        return $this->execute($method, $params);
127
+    }
128
+
129
+    /**
130
+     * Execute an RPC command
131
+     *
132
+     * @param  string $method  Procedure name
133
+     * @param  array  $params  Procedure arguments
134
+     * @return mixed
135
+     */
136
+    public function execute($method, array $params = array())
137
+    {
138
+        $id = mt_rand();
139
+
140
+        $payload = array(
141
+            'jsonrpc' => '2.0',
142
+            'method' => $method,
143
+            'id' => $id,
144
+        );
145
+
146
+        if (!empty($params)) {
147
+            $payload['params'] = $params;
148
+        }
149
+
150
+        $result = $this->send_request($payload, $method != 'system.keygen');
151
+
152
+        if (isset($result['id']) && $result['id'] == $id && array_key_exists('result', $result)) {
153
+            return $result['result'];
154
+        }
155
+        else if (isset($result['error'])) {
156
+            $this->_debug('ERROR', $result);
157
+        }
158
+
159
+        return null;
160
+    }
161
+
162
+    /**
163
+     * Do the HTTP request
164
+     *
165
+     * @param  string  $payload  Data to send
166
+     */
167
+    protected function send_request($payload, $sign = true)
168
+    {
169
+        try {
170
+            $payload_ = json_encode($payload);
171
+
172
+            // add request signature
173
+            if ($sign && !empty($this->secret)) {
174
+                $this->headers['X-Request-Sign'] = $this->request_signature($payload_);
175
+            }
176
+            else if ($this->headers['X-Request-Sign']) {
177
+                unset($this->headers['X-Request-Sign']);
178
+            }
179
+
180
+            $this->_debug('REQUEST', $payload, $this->headers);
181
+            $request = libkolab::http_request($this->url, 'POST', array('timeout' => $this->timeout));
182
+            $request->setHeader($this->headers);
183
+            $request->setAuth($this->username, $this->password);
184
+            $request->setBody($payload_);
185
+
186
+            $response = $request->send();
187
+
188
+            if ($response->getStatus() == 200) {
189
+                $result = json_decode($response->getBody(), true);
190
+                $this->_debug('RESPONSE', $result);
191
+            }
192
+            else {
193
+                throw new Exception(sprintf("HTTP %d %s", $response->getStatus(), $response->getReasonPhrase()));
194
+            }
195
+        }
196
+        catch (Exception $e) {
197
+            rcube::raise_error(array(
198
+                'code' => 500,
199
+                'type' => 'php',
200
+                'message' => "Bonnie API request failed: " . $e->getMessage(),
201
+            ), true);
202
+
203
+            return array('id' => $payload['id'], 'error' => $e->getMessage(), 'code' => -32000);
204
+        }
205
+
206
+        return is_array($result) ? $result : array();
207
+    }
208
+
209
+    /**
210
+     * Compute the hmac signature for the current event payload using
211
+     * the secret key configured for this API client
212
+     *
213
+     * @param string $data The request payload data
214
+     * @return string The request signature
215
+     */
216
+    protected function request_signature($data)
217
+    {
218
+        // TODO: get the session key with a system.keygen call
219
+        return hash_hmac('sha256', $this->headers['X-Request-User'] . ':' . $data, $this->secret);
220
+    }
221
+
222
+    /**
223
+     * Write debug log
224
+     */
225
+    protected function _debug(/* $message, $data1, data2, ...*/)
226
+    {
227
+        if (!$this->debug)
228
+            return;
229
+
230
+        $args = func_get_args();
231
+
232
+        $msg = array();
233
+        foreach ($args as $arg) {
234
+            $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
235
+        }
236
+
237
+        rcube::write_log('bonnie', join(";\n", $msg));
238
+    }
239
+
240
+}
241
\ No newline at end of file
242
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_date_recurrence.php Added
137
 
1
@@ -0,0 +1,135 @@
2
+<?php
3
+
4
+/**
5
+ * Recurrence computation class for xcal-based Kolab format objects
6
+ *
7
+ * Utility class to compute instances of recurring events.
8
+ * It requires the libcalendaring PHP module to be installed and loaded.
9
+ *
10
+ * @version @package_version@
11
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
12
+ *
13
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
14
+ *
15
+ * This program is free software: you can redistribute it and/or modify
16
+ * it under the terms of the GNU Affero General Public License as
17
+ * published by the Free Software Foundation, either version 3 of the
18
+ * License, or (at your option) any later version.
19
+ *
20
+ * This program is distributed in the hope that it will be useful,
21
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
+ * GNU Affero General Public License for more details.
24
+ *
25
+ * You should have received a copy of the GNU Affero General Public License
26
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
27
+ */
28
+class kolab_date_recurrence
29
+{
30
+    private /* EventCal */ $engine;
31
+    private /* kolab_format_xcal */ $object;
32
+    private /* DateTime */ $start;
33
+    private /* DateTime */ $next;
34
+    private /* cDateTime */ $cnext;
35
+    private /* DateInterval */ $duration;
36
+
37
+    /**
38
+     * Default constructor
39
+     *
40
+     * @param array The Kolab object to operate on
41
+     */
42
+    function __construct($object)
43
+    {
44
+        $data = $object->to_array();
45
+
46
+        $this->object = $object;
47
+        $this->engine = $object->to_libcal();
48
+        $this->start = $this->next = $data['start'];
49
+        $this->cnext = kolab_format::get_datetime($this->next);
50
+
51
+        if (is_object($data['start']) && is_object($data['end']))
52
+            $this->duration = $data['start']->diff($data['end']);
53
+        else
54
+            $this->duration = new DateInterval('PT' . ($data['end'] - $data['start']) . 'S');
55
+    }
56
+
57
+    /**
58
+     * Get date/time of the next occurence of this event
59
+     *
60
+     * @param boolean Return a Unix timestamp instead of a DateTime object
61
+     * @return mixed  DateTime object/unix timestamp or False if recurrence ended
62
+     */
63
+    public function next_start($timestamp = false)
64
+    {
65
+        $time = false;
66
+
67
+        if ($this->engine && $this->next) {
68
+            if (($cnext = new cDateTime($this->engine->getNextOccurence($this->cnext))) && $cnext->isValid()) {
69
+                $next = kolab_format::php_datetime($cnext);
70
+                $time = $timestamp ? $next->format('U') : $next;
71
+                $this->cnext = $cnext;
72
+                $this->next = $next;
73
+            }
74
+        }
75
+
76
+        return $time;
77
+    }
78
+
79
+    /**
80
+     * Get the next recurring instance of this event
81
+     *
82
+     * @return mixed Array with event properties or False if recurrence ended
83
+     */
84
+    public function next_instance()
85
+    {
86
+        if ($next_start = $this->next_start()) {
87
+            $next_end = clone $next_start;
88
+            $next_end->add($this->duration);
89
+
90
+            $next = $this->object->to_array();
91
+            $next['recurrence_id'] = $next_start->format('Y-m-d');
92
+            $next['start'] = $next_start;
93
+            $next['end'] = $next_end;
94
+            unset($next['_formatobj']);
95
+
96
+            return $next;
97
+        }
98
+
99
+        return false;
100
+    }
101
+
102
+    /**
103
+     * Get the end date of the occurence of this recurrence cycle
104
+     *
105
+     * @return DateTime|bool End datetime of the last event or False if recurrence exceeds limit
106
+     */
107
+    public function end()
108
+    {
109
+        $event = $this->object->to_array();
110
+
111
+        // recurrence end date is given
112
+        if ($event['recurrence']['UNTIL'] instanceof DateTime) {
113
+            return $event['recurrence']['UNTIL'];
114
+        }
115
+
116
+        // let libkolab do the work
117
+        if ($this->engine && ($cend = $this->engine->getLastOccurrence()) && ($end_dt = kolab_format::php_datetime(new cDateTime($cend)))) {
118
+            return $end_dt;
119
+        }
120
+
121
+        // determine a reasonable end date if none given
122
+        if (!$event['recurrence']['COUNT'] && $event['end'] instanceof DateTime) {
123
+          switch ($event['recurrence']['FREQ']) {
124
+            case 'YEARLY':  $intvl = 'P100Y'; break;
125
+            case 'MONTHLY': $intvl = 'P20Y';  break;
126
+            default:        $intvl = 'P10Y';  break;
127
+          }
128
+
129
+          $end_dt = clone $event['end'];
130
+          $end_dt->add(new DateInterval($intvl));
131
+          return $end_dt;
132
+        }
133
+
134
+        return false;
135
+    }
136
+}
137
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_format.php Added
701
 
1
@@ -0,0 +1,699 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab format model class wrapping libkolabxml bindings
6
+ *
7
+ * Abstract base class for different Kolab groupware objects read from/written
8
+ * to the new Kolab 3 format using the PHP bindings of libkolabxml.
9
+ *
10
+ * @version @package_version@
11
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
12
+ *
13
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
14
+ *
15
+ * This program is free software: you can redistribute it and/or modify
16
+ * it under the terms of the GNU Affero General Public License as
17
+ * published by the Free Software Foundation, either version 3 of the
18
+ * License, or (at your option) any later version.
19
+ *
20
+ * This program is distributed in the hope that it will be useful,
21
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
+ * GNU Affero General Public License for more details.
24
+ *
25
+ * You should have received a copy of the GNU Affero General Public License
26
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
27
+ */
28
+
29
+abstract class kolab_format
30
+{
31
+    public static $timezone;
32
+
33
+    public /*abstract*/ $CTYPE;
34
+    public /*abstract*/ $CTYPEv2;
35
+
36
+    protected /*abstract*/ $objclass;
37
+    protected /*abstract*/ $read_func;
38
+    protected /*abstract*/ $write_func;
39
+
40
+    protected $obj;
41
+    protected $data;
42
+    protected $xmldata;
43
+    protected $xmlobject;
44
+    protected $formaterror;
45
+    protected $loaded = false;
46
+    protected $version = '3.0';
47
+
48
+    const KTYPE_PREFIX = 'application/x-vnd.kolab.';
49
+    const PRODUCT_ID   = 'Roundcube-libkolab-1.1';
50
+
51
+    // mapping table for valid PHP timezones not supported by libkolabxml
52
+    // basically the entire list of ftp://ftp.iana.org/tz/data/backward
53
+    protected static $timezone_map = array(
54
+        'Africa/Asmera' => 'Africa/Asmara',
55
+        'Africa/Timbuktu' => 'Africa/Abidjan',
56
+        'America/Argentina/ComodRivadavia' => 'America/Argentina/Catamarca',
57
+        'America/Atka' => 'America/Adak',
58
+        'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
59
+        'America/Catamarca' => 'America/Argentina/Catamarca',
60
+        'America/Coral_Harbour' => 'America/Atikokan',
61
+        'America/Cordoba' => 'America/Argentina/Cordoba',
62
+        'America/Ensenada' => 'America/Tijuana',
63
+        'America/Fort_Wayne' => 'America/Indiana/Indianapolis',
64
+        'America/Indianapolis' => 'America/Indiana/Indianapolis',
65
+        'America/Jujuy' => 'America/Argentina/Jujuy',
66
+        'America/Knox_IN' => 'America/Indiana/Knox',
67
+        'America/Louisville' => 'America/Kentucky/Louisville',
68
+        'America/Mendoza' => 'America/Argentina/Mendoza',
69
+        'America/Porto_Acre' => 'America/Rio_Branco',
70
+        'America/Rosario' => 'America/Argentina/Cordoba',
71
+        'America/Virgin' => 'America/Port_of_Spain',
72
+        'Asia/Ashkhabad' => 'Asia/Ashgabat',
73
+        'Asia/Calcutta' => 'Asia/Kolkata',
74
+        'Asia/Chungking' => 'Asia/Shanghai',
75
+        'Asia/Dacca' => 'Asia/Dhaka',
76
+        'Asia/Katmandu' => 'Asia/Kathmandu',
77
+        'Asia/Macao' => 'Asia/Macau',
78
+        'Asia/Saigon' => 'Asia/Ho_Chi_Minh',
79
+        'Asia/Tel_Aviv' => 'Asia/Jerusalem',
80
+        'Asia/Thimbu' => 'Asia/Thimphu',
81
+        'Asia/Ujung_Pandang' => 'Asia/Makassar',
82
+        'Asia/Ulan_Bator' => 'Asia/Ulaanbaatar',
83
+        'Atlantic/Faeroe' => 'Atlantic/Faroe',
84
+        'Atlantic/Jan_Mayen' => 'Europe/Oslo',
85
+        'Australia/ACT' => 'Australia/Sydney',
86
+        'Australia/Canberra' => 'Australia/Sydney',
87
+        'Australia/LHI' => 'Australia/Lord_Howe',
88
+        'Australia/NSW' => 'Australia/Sydney',
89
+        'Australia/North' => 'Australia/Darwin',
90
+        'Australia/Queensland' => 'Australia/Brisbane',
91
+        'Australia/South' => 'Australia/Adelaide',
92
+        'Australia/Tasmania' => 'Australia/Hobart',
93
+        'Australia/Victoria' => 'Australia/Melbourne',
94
+        'Australia/West' => 'Australia/Perth',
95
+        'Australia/Yancowinna' => 'Australia/Broken_Hill',
96
+        'Brazil/Acre' => 'America/Rio_Branco',
97
+        'Brazil/DeNoronha' => 'America/Noronha',
98
+        'Brazil/East' => 'America/Sao_Paulo',
99
+        'Brazil/West' => 'America/Manaus',
100
+        'Canada/Atlantic' => 'America/Halifax',
101
+        'Canada/Central' => 'America/Winnipeg',
102
+        'Canada/East-Saskatchewan' => 'America/Regina',
103
+        'Canada/Eastern' => 'America/Toronto',
104
+        'Canada/Mountain' => 'America/Edmonton',
105
+        'Canada/Newfoundland' => 'America/St_Johns',
106
+        'Canada/Pacific' => 'America/Vancouver',
107
+        'Canada/Saskatchewan' => 'America/Regina',
108
+        'Canada/Yukon' => 'America/Whitehorse',
109
+        'Chile/Continental' => 'America/Santiago',
110
+        'Chile/EasterIsland' => 'Pacific/Easter',
111
+        'Cuba' => 'America/Havana',
112
+        'Egypt' => 'Africa/Cairo',
113
+        'Eire' => 'Europe/Dublin',
114
+        'Europe/Belfast' => 'Europe/London',
115
+        'Europe/Tiraspol' => 'Europe/Chisinau',
116
+        'GB' => 'Europe/London',
117
+        'GB-Eire' => 'Europe/London',
118
+        'Greenwich' => 'Etc/GMT',
119
+        'Hongkong' => 'Asia/Hong_Kong',
120
+        'Iceland' => 'Atlantic/Reykjavik',
121
+        'Iran' => 'Asia/Tehran',
122
+        'Israel' => 'Asia/Jerusalem',
123
+        'Jamaica' => 'America/Jamaica',
124
+        'Japan' => 'Asia/Tokyo',
125
+        'Kwajalein' => 'Pacific/Kwajalein',
126
+        'Libya' => 'Africa/Tripoli',
127
+        'Mexico/BajaNorte' => 'America/Tijuana',
128
+        'Mexico/BajaSur' => 'America/Mazatlan',
129
+        'Mexico/General' => 'America/Mexico_City',
130
+        'NZ' => 'Pacific/Auckland',
131
+        'NZ-CHAT' => 'Pacific/Chatham',
132
+        'Navajo' => 'America/Denver',
133
+        'PRC' => 'Asia/Shanghai',
134
+        'Pacific/Ponape' => 'Pacific/Pohnpei',
135
+        'Pacific/Samoa' => 'Pacific/Pago_Pago',
136
+        'Pacific/Truk' => 'Pacific/Chuuk',
137
+        'Pacific/Yap' => 'Pacific/Chuuk',
138
+        'Poland' => 'Europe/Warsaw',
139
+        'Portugal' => 'Europe/Lisbon',
140
+        'ROC' => 'Asia/Taipei',
141
+        'ROK' => 'Asia/Seoul',
142
+        'Singapore' => 'Asia/Singapore',
143
+        'Turkey' => 'Europe/Istanbul',
144
+        'UCT' => 'Etc/UCT',
145
+        'US/Alaska' => 'America/Anchorage',
146
+        'US/Aleutian' => 'America/Adak',
147
+        'US/Arizona' => 'America/Phoenix',
148
+        'US/Central' => 'America/Chicago',
149
+        'US/East-Indiana' => 'America/Indiana/Indianapolis',
150
+        'US/Eastern' => 'America/New_York',
151
+        'US/Hawaii' => 'Pacific/Honolulu',
152
+        'US/Indiana-Starke' => 'America/Indiana/Knox',
153
+        'US/Michigan' => 'America/Detroit',
154
+        'US/Mountain' => 'America/Denver',
155
+        'US/Pacific' => 'America/Los_Angeles',
156
+        'US/Samoa' => 'Pacific/Pago_Pago',
157
+        'Universal' => 'Etc/UTC',
158
+        'W-SU' => 'Europe/Moscow',
159
+        'Zulu' => 'Etc/UTC',
160
+    );
161
+
162
+    /**
163
+     * Factory method to instantiate a kolab_format object of the given type and version
164
+     *
165
+     * @param string Object type to instantiate
166
+     * @param float  Format version
167
+     * @param string Cached xml data to initialize with
168
+     * @return object kolab_format
169
+     */
170
+    public static function factory($type, $version = '3.0', $xmldata = null)
171
+    {
172
+        if (!isset(self::$timezone))
173
+            self::$timezone = new DateTimeZone('UTC');
174
+
175
+        if (!self::supports($version))
176
+            return PEAR::raiseError("No support for Kolab format version " . $version);
177
+
178
+        $type = preg_replace('/configuration\.[a-z._]+$/', 'configuration', $type);
179
+        $suffix = preg_replace('/[^a-z]+/', '', $type);
180
+        $classname = 'kolab_format_' . $suffix;
181
+        if (class_exists($classname))
182
+            return new $classname($xmldata, $version);
183
+
184
+        return PEAR::raiseError("Failed to load Kolab Format wrapper for type " . $type);
185
+    }
186
+
187
+    /**
188
+     * Determine support for the given format version
189
+     *
190
+     * @param float Format version to check
191
+     * @return boolean True if supported, False otherwise
192
+     */
193
+    public static function supports($version)
194
+    {
195
+        if ($version == '2.0')
196
+            return class_exists('kolabobject');
197
+        // default is version 3
198
+        return class_exists('kolabformat');
199
+    }
200
+
201
+    /**
202
+     * Convert the given date/time value into a cDateTime object
203
+     *
204
+     * @param mixed         Date/Time value either as unix timestamp, date string or PHP DateTime object
205
+     * @param DateTimeZone  The timezone the date/time is in. Use global default if Null, local time if False
206
+     * @param boolean       True of the given date has no time component
207
+     * @return object       The libkolabxml date/time object
208
+     */
209
+    public static function get_datetime($datetime, $tz = null, $dateonly = false)
210
+    {
211
+        // use timezone information from datetime of global setting
212
+        if (!$tz && $tz !== false) {
213
+            if ($datetime instanceof DateTime)
214
+                $tz = $datetime->getTimezone();
215
+            if (!$tz)
216
+                $tz = self::$timezone;
217
+        }
218
+        $result = new cDateTime();
219
+
220
+        try {
221
+            // got a unix timestamp (in UTC)
222
+            if (is_numeric($datetime)) {
223
+                $datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
224
+                if ($tz) $datetime->setTimezone($tz);
225
+            }
226
+            else if (is_string($datetime) && strlen($datetime)) {
227
+                $datetime = $tz ? new DateTime($datetime, $tz) : new DateTime($datetime);
228
+            }
229
+        }
230
+        catch (Exception $e) {}
231
+
232
+        if ($datetime instanceof DateTime) {
233
+            $result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
234
+
235
+            if (!$dateonly)
236
+                $result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
237
+
238
+            if ($tz && in_array($tz->getName(), array('UTC', 'GMT', '+00:00', 'Z'))) {
239
+                $result->setUTC(true);
240
+            }
241
+            else if ($tz !== false) {
242
+                $tzid = $tz->getName();
243
+                if (array_key_exists($tzid, self::$timezone_map))
244
+                    $tzid = self::$timezone_map[$tzid];
245
+                $result->setTimezone($tzid);
246
+            }
247
+        }
248
+
249
+        return $result;
250
+    }
251
+
252
+    /**
253
+     * Convert the given cDateTime into a PHP DateTime object
254
+     *
255
+     * @param object cDateTime  The libkolabxml datetime object
256
+     * @return object DateTime  PHP datetime instance
257
+     */
258
+    public static function php_datetime($cdt)
259
+    {
260
+        if (!is_object($cdt) || !$cdt->isValid())
261
+            return null;
262
+
263
+        $d = new DateTime;
264
+        $d->setTimezone(self::$timezone);
265
+
266
+        try {
267
+            if ($tzs = $cdt->timezone()) {
268
+                $tz = new DateTimeZone($tzs);
269
+                $d->setTimezone($tz);
270
+            }
271
+            else if ($cdt->isUTC()) {
272
+                $d->setTimezone(new DateTimeZone('UTC'));
273
+            }
274
+        }
275
+        catch (Exception $e) { }
276
+
277
+        $d->setDate($cdt->year(), $cdt->month(), $cdt->day());
278
+
279
+        if ($cdt->isDateOnly()) {
280
+            $d->_dateonly = true;
281
+            $d->setTime(12, 0, 0);  // set time to noon to avoid timezone troubles
282
+        }
283
+        else {
284
+            $d->setTime($cdt->hour(), $cdt->minute(), $cdt->second());
285
+        }
286
+
287
+        return $d;
288
+    }
289
+
290
+    /**
291
+     * Convert a libkolabxml vector to a PHP array
292
+     *
293
+     * @param object vector Object
294
+     * @return array Indexed array containing vector elements
295
+     */
296
+    public static function vector2array($vec, $max = PHP_INT_MAX)
297
+    {
298
+        $arr = array();
299
+        for ($i=0; $i < $vec->size() && $i < $max; $i++)
300
+            $arr[] = $vec->get($i);
301
+        return $arr;
302
+    }
303
+
304
+    /**
305
+     * Build a libkolabxml vector (string) from a PHP array
306
+     *
307
+     * @param array Array with vector elements
308
+     * @return object vectors
309
+     */
310
+    public static function array2vector($arr)
311
+    {
312
+        $vec = new vectors;
313
+        foreach ((array)$arr as $val) {
314
+            if (strlen($val))
315
+                $vec->push($val);
316
+        }
317
+        return $vec;
318
+    }
319
+
320
+    /**
321
+     * Parse the X-Kolab-Type header from MIME messages and return the object type in short form
322
+     *
323
+     * @param string X-Kolab-Type header value
324
+     * @return string Kolab object type (contact,event,task,note,etc.)
325
+     */
326
+    public static function mime2object_type($x_kolab_type)
327
+    {
328
+        return preg_replace(
329
+            array('/dictionary.[a-z.]+$/', '/contact.distlist$/'),
330
+            array( 'dictionary',            'distribution-list'),
331
+            substr($x_kolab_type, strlen(self::KTYPE_PREFIX))
332
+        );
333
+    }
334
+
335
+
336
+    /**
337
+     * Default constructor of all kolab_format_* objects
338
+     */
339
+    public function __construct($xmldata = null, $version = null)
340
+    {
341
+        $this->obj = new $this->objclass;
342
+        $this->xmldata = $xmldata;
343
+
344
+        if ($version)
345
+            $this->version = $version;
346
+
347
+        // use libkolab module if available
348
+        if (class_exists('kolabobject'))
349
+            $this->xmlobject = new XMLObject();
350
+    }
351
+
352
+    /**
353
+     * Check for format errors after calling kolabformat::write*()
354
+     *
355
+     * @return boolean True if there were errors, False if OK
356
+     */
357
+    protected function format_errors()
358
+    {
359
+        $ret = $log = false;
360
+        switch (kolabformat::error()) {
361
+            case kolabformat::NoError:
362
+                $ret = false;
363
+                break;
364
+            case kolabformat::Warning:
365
+                $ret = false;
366
+                $uid = is_object($this->obj) ? $this->obj->uid() : $this->data['uid'];
367
+                $log = "Warning @ $uid";
368
+                break;
369
+            default:
370
+                $ret = true;
371
+                $log = "Error";
372
+        }
373
+
374
+        if ($log && !isset($this->formaterror)) {
375
+            rcube::raise_error(array(
376
+                'code' => 660,
377
+                'type' => 'php',
378
+                'file' => __FILE__,
379
+                'line' => __LINE__,
380
+                'message' => "kolabformat $log: " . kolabformat::errorMessage(),
381
+            ), true);
382
+
383
+            $this->formaterror = $ret;
384
+        }
385
+
386
+        return $ret;
387
+    }
388
+
389
+    /**
390
+     * Save the last generated UID to the object properties.
391
+     * Should be called after kolabformat::writeXXXX();
392
+     */
393
+    protected function update_uid()
394
+    {
395
+        // get generated UID
396
+        if (!$this->data['uid']) {
397
+            if ($this->xmlobject) {
398
+                $this->data['uid'] = $this->xmlobject->getSerializedUID();
399
+            }
400
+            if (empty($this->data['uid'])) {
401
+                $this->data['uid'] = kolabformat::getSerializedUID();
402
+            }
403
+            $this->obj->setUid($this->data['uid']);
404
+        }
405
+    }
406
+
407
+    /**
408
+     * Initialize libkolabxml object with cached xml data
409
+     */
410
+    protected function init()
411
+    {
412
+        if (!$this->loaded) {
413
+            if ($this->xmldata) {
414
+                $this->load($this->xmldata);
415
+                $this->xmldata = null;
416
+            }
417
+            $this->loaded = true;
418
+        }
419
+    }
420
+
421
+    /**
422
+     * Get constant value for libkolab's version parameter
423
+     *
424
+     * @param float Version value to convert
425
+     * @return int Constant value of either kolabobject::KolabV2 or kolabobject::KolabV3 or false if kolabobject module isn't available
426
+     */
427
+    protected function libversion($v = null)
428
+    {
429
+        if (class_exists('kolabobject')) {
430
+            $version = $v ?: $this->version;
431
+            if ($version <= '2.0')
432
+                return kolabobject::KolabV2;
433
+            else
434
+                return kolabobject::KolabV3;
435
+        }
436
+
437
+        return false;
438
+    }
439
+
440
+    /**
441
+     * Determine the correct libkolab(xml) wrapper function for the given call
442
+     * depending on the available PHP modules
443
+     */
444
+    protected function libfunc($func)
445
+    {
446
+        if (is_array($func) || strpos($func, '::'))
447
+            return $func;
448
+        else if (class_exists('kolabobject'))
449
+            return array($this->xmlobject, $func);
450
+        else
451
+            return 'kolabformat::' . $func;
452
+    }
453
+
454
+    /**
455
+     * Direct getter for object properties
456
+     */
457
+    public function __get($var)
458
+    {
459
+        return $this->data[$var];
460
+    }
461
+
462
+    /**
463
+     * Load Kolab object data from the given XML block
464
+     *
465
+     * @param string XML data
466
+     * @return boolean True on success, False on failure
467
+     */
468
+    public function load($xml)
469
+    {
470
+        $this->formaterror = null;
471
+        $read_func = $this->libfunc($this->read_func);
472
+
473
+        if (is_array($read_func))
474
+            $r = call_user_func($read_func, $xml, $this->libversion());
475
+        else
476
+            $r = call_user_func($read_func, $xml, false);
477
+
478
+        if (is_resource($r))
479
+            $this->obj = new $this->objclass($r);
480
+        else if (is_a($r, $this->objclass))
481
+            $this->obj = $r;
482
+
483
+        $this->loaded = !$this->format_errors();
484
+    }
485
+
486
+    /**
487
+     * Write object data to XML format
488
+     *
489
+     * @param float Format version to write
490
+     * @return string XML data
491
+     */
492
+    public function write($version = null)
493
+    {
494
+        $this->formaterror = null;
495
+
496
+        $this->init();
497
+        $write_func = $this->libfunc($this->write_func);
498
+        if (is_array($write_func))
499
+            $this->xmldata = call_user_func($write_func, $this->obj, $this->libversion($version), self::PRODUCT_ID);
500
+        else
501
+            $this->xmldata = call_user_func($write_func, $this->obj, self::PRODUCT_ID);
502
+
503
+        if (!$this->format_errors())
504
+            $this->update_uid();
505
+        else
506
+            $this->xmldata = null;
507
+
508
+        return $this->xmldata;
509
+    }
510
+
511
+    /**
512
+     * Set properties to the kolabformat object
513
+     *
514
+     * @param array  Object data as hash array
515
+     */
516
+    public function set(&$object)
517
+    {
518
+        $this->init();
519
+
520
+        if (!empty($object['uid']))
521
+            $this->obj->setUid($object['uid']);
522
+
523
+        // set some automatic values if missing
524
+        if (empty($object['created']) && method_exists($this->obj, 'setCreated')) {
525
+            $cdt = $this->obj->created();
526
+            $object['created'] = $cdt && $cdt->isValid() ? self::php_datetime($cdt) : new DateTime('now', new DateTimeZone('UTC'));
527
+            if (!$cdt || !$cdt->isValid())
528
+                $this->obj->setCreated(self::get_datetime($object['created']));
529
+        }
530
+
531
+        $object['changed'] = new DateTime('now', new DateTimeZone('UTC'));
532
+        $this->obj->setLastModified(self::get_datetime($object['changed']));
533
+
534
+        // Save custom properties of the given object
535
+        if (isset($object['x-custom']) && method_exists($this->obj, 'setCustomProperties')) {
536
+            $vcustom = new vectorcs;
537
+            foreach ((array)$object['x-custom'] as $cp) {
538
+                if (is_array($cp))
539
+                    $vcustom->push(new CustomProperty($cp[0], $cp[1]));
540
+            }
541
+            $this->obj->setCustomProperties($vcustom);
542
+        }
543
+        // load custom properties from XML for caching (#2238) if method exists (#3125)
544
+        else if (method_exists($this->obj, 'customProperties')) {
545
+            $object['x-custom'] = array();
546
+            $vcustom = $this->obj->customProperties();
547
+            for ($i=0; $i < $vcustom->size(); $i++) {
548
+                $cp = $vcustom->get($i);
549
+                $object['x-custom'][] = array($cp->identifier, $cp->value);
550
+            }
551
+        }
552
+    }
553
+
554
+    /**
555
+     * Convert the Kolab object into a hash array data structure
556
+     *
557
+     * @param array Additional data for merge
558
+     *
559
+     * @return array  Kolab object data as hash array
560
+     */
561
+    public function to_array($data = array())
562
+    {
563
+        $this->init();
564
+
565
+        // read object properties into local data object
566
+        $object = array(
567
+            'uid'     => $this->obj->uid(),
568
+            'changed' => self::php_datetime($this->obj->lastModified()),
569
+        );
570
+
571
+        // not all container support the created property
572
+        if (method_exists($this->obj, 'created')) {
573
+            $object['created'] = self::php_datetime($this->obj->created());
574
+        }
575
+
576
+        // read custom properties
577
+        if (method_exists($this->obj, 'customProperties')) {
578
+            $vcustom = $this->obj->customProperties();
579
+            for ($i=0; $i < $vcustom->size(); $i++) {
580
+                $cp = $vcustom->get($i);
581
+                $object['x-custom'][] = array($cp->identifier, $cp->value);
582
+            }
583
+        }
584
+
585
+        // merge with additional data, e.g. attachments from the message
586
+        if ($data) {
587
+            foreach ($data as $idx => $value) {
588
+                if (is_array($value)) {
589
+                    $object[$idx] = array_merge((array)$object[$idx], $value);
590
+                }
591
+                else {
592
+                    $object[$idx] = $value;
593
+                }
594
+            }
595
+        }
596
+
597
+        return $object;
598
+    }
599
+
600
+    /**
601
+     * Object validation method to be implemented by derived classes
602
+     */
603
+    abstract public function is_valid();
604
+
605
+    /**
606
+     * Callback for kolab_storage_cache to get object specific tags to cache
607
+     *
608
+     * @return array List of tags to save in cache
609
+     */
610
+    public function get_tags()
611
+    {
612
+        return array();
613
+    }
614
+
615
+    /**
616
+     * Callback for kolab_storage_cache to get words to index for fulltext search
617
+     *
618
+     * @return array List of words to save in cache
619
+     */
620
+    public function get_words()
621
+    {
622
+        return array();
623
+    }
624
+
625
+    /**
626
+     * Utility function to extract object attachment data
627
+     *
628
+     * @param array Hash array reference to append attachment data into
629
+     */
630
+    public function get_attachments(&$object)
631
+    {
632
+        $this->init();
633
+
634
+        // handle attachments
635
+        $vattach = $this->obj->attachments();
636
+        for ($i=0; $i < $vattach->size(); $i++) {
637
+            $attach = $vattach->get($i);
638
+
639
+            // skip cid: attachments which are mime message parts handled by kolab_storage_folder
640
+            if (substr($attach->uri(), 0, 4) != 'cid:' && $attach->label()) {
641
+                $name    = $attach->label();
642
+                $key     = $name . (isset($object['_attachments'][$name]) ? '.'.$i : '');
643
+                $content = $attach->data();
644
+                $object['_attachments'][$key] = array(
645
+                    'id'       => 'i:'.$i,
646
+                    'name'     => $name,
647
+                    'mimetype' => $attach->mimetype(),
648
+                    'size'     => strlen($content),
649
+                    'content'  => $content,
650
+                );
651
+            }
652
+            else if (in_array(substr($attach->uri(), 0, 4), array('http','imap'))) {
653
+                $object['links'][] = $attach->uri();
654
+            }
655
+        }
656
+    }
657
+
658
+    /**
659
+     * Utility function to set attachment properties to the kolabformat object
660
+     *
661
+     * @param array  Object data as hash array
662
+     * @param boolean True to always overwrite attachment information
663
+     */
664
+    protected function set_attachments($object, $write = true)
665
+    {
666
+        // save attachments
667
+        $vattach = new vectorattachment;
668
+        foreach ((array) $object['_attachments'] as $cid => $attr) {
669
+            if (empty($attr))
670
+                continue;
671
+            $attach = new Attachment;
672
+            $attach->setLabel((string)$attr['name']);
673
+            $attach->setUri('cid:' . $cid, $attr['mimetype'] ?: 'application/octet-stream');
674
+            if ($attach->isValid()) {
675
+                $vattach->push($attach);
676
+                $write = true;
677
+            }
678
+            else {
679
+                rcube::raise_error(array(
680
+                    'code' => 660,
681
+                    'type' => 'php',
682
+                    'file' => __FILE__,
683
+                    'line' => __LINE__,
684
+                    'message' => "Invalid attributes for attachment $cid: " . var_export($attr, true),
685
+                ), true);
686
+            }
687
+        }
688
+
689
+        foreach ((array) $object['links'] as $link) {
690
+            $attach = new Attachment;
691
+            $attach->setUri($link, 'unknown');
692
+            $vattach->push($attach);
693
+            $write = true;
694
+        }
695
+
696
+        if ($write) {
697
+            $this->obj->setAttachments($vattach);
698
+        }
699
+    }
700
+}
701
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php Added
286
 
1
@@ -0,0 +1,284 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab Configuration data model class
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+
26
+class kolab_format_configuration extends kolab_format
27
+{
28
+    public $CTYPE   = 'application/vnd.kolab+xml';
29
+    public $CTYPEv2 = 'application/x-vnd.kolab.configuration';
30
+
31
+    protected $objclass   = 'Configuration';
32
+    protected $read_func  = 'readConfiguration';
33
+    protected $write_func = 'writeConfiguration';
34
+
35
+    private $type_map = array(
36
+        'category'    => Configuration::TypeCategoryColor,
37
+        'dictionary'  => Configuration::TypeDictionary,
38
+        'file_driver' => Configuration::TypeFileDriver,
39
+        'relation'    => Configuration::TypeRelation,
40
+        'snippet'     => Configuration::TypeSnippet,
41
+    );
42
+
43
+    private $driver_settings_fields = array('host', 'port', 'username', 'password');
44
+
45
+    /**
46
+     * Set properties to the kolabformat object
47
+     *
48
+     * @param array  Object data as hash array
49
+     */
50
+    public function set(&$object)
51
+    {
52
+        // read type-specific properties
53
+        switch ($object['type']) {
54
+        case 'dictionary':
55
+            $dict = new Dictionary($object['language']);
56
+            $dict->setEntries(self::array2vector($object['e']));
57
+            $this->obj = new Configuration($dict);
58
+            break;
59
+
60
+        case 'category':
61
+            // TODO: implement this
62
+            $categories = new vectorcategorycolor;
63
+            $this->obj = new Configuration($categories);
64
+            break;
65
+
66
+        case 'file_driver':
67
+            $driver = new FileDriver($object['driver'], $object['title']);
68
+
69
+            $driver->setEnabled((bool) $object['enabled']);
70
+
71
+            foreach ($this->driver_settings_fields as $field) {
72
+                $value = $object[$field];
73
+                if ($value !== null) {
74
+                    $driver->{'set' . ucfirst($field)}($value);
75
+                }
76
+            }
77
+
78
+            $this->obj = new Configuration($driver);
79
+            break;
80
+
81
+        case 'relation':
82
+            $relation = new Relation(strval($object['name']), strval($object['category']));
83
+
84
+            if ($object['color']) {
85
+                $relation->setColor($object['color']);
86
+            }
87
+            if ($object['parent']) {
88
+                $relation->setParent($object['parent']);
89
+            }
90
+            if ($object['iconName']) {
91
+                $relation->setIconName($object['iconName']);
92
+            }
93
+            if ($object['priority'] > 0) {
94
+                $relation->setPriority((int) $object['priority']);
95
+            }
96
+            if (!empty($object['members'])) {
97
+                $relation->setMembers(self::array2vector($object['members']));
98
+            }
99
+
100
+            $this->obj = new Configuration($relation);
101
+            break;
102
+
103
+        case 'snippet':
104
+            $collection = new SnippetCollection($object['name']);
105
+            $snippets   = new vectorsnippets;
106
+
107
+            foreach ((array) $object['snippets'] as $item) {
108
+                $snippet = new snippet($item['name'], $item['text']);
109
+                $snippet->setTextType(strtolower($item['type']) == 'html' ? Snippet::HTML : Snippet::Plain);
110
+                if ($item['shortcut']) {
111
+                    $snippet->setShortCut($item['shortcut']);
112
+                }
113
+
114
+                $snippets->push($snippet);
115
+            }
116
+
117
+            $collection->setSnippets($snippets);
118
+
119
+            $this->obj = new Configuration($collection);
120
+            break;
121
+
122
+        default:
123
+            return false;
124
+        }
125
+
126
+        // adjust content-type string
127
+        $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type'];
128
+
129
+        // reset old object data, otherwise set() will overwrite current data (#4095)
130
+        $this->xmldata = null;
131
+        // set common object properties
132
+        parent::set($object);
133
+
134
+        // cache this data
135
+        $this->data = $object;
136
+        unset($this->data['_formatobj']);
137
+    }
138
+
139
+    /**
140
+     *
141
+     */
142
+    public function is_valid()
143
+    {
144
+        return $this->data || (is_object($this->obj) && $this->obj->isValid());
145
+    }
146
+
147
+    /**
148
+     * Convert the Configuration object into a hash array data structure
149
+     *
150
+     * @param array Additional data for merge
151
+     *
152
+     * @return array  Config object data as hash array
153
+     */
154
+    public function to_array($data = array())
155
+    {
156
+        // return cached result
157
+        if (!empty($this->data)) {
158
+            return $this->data;
159
+        }
160
+
161
+        // read common object props into local data object
162
+        $object = parent::to_array($data);
163
+
164
+        $type_map = array_flip($this->type_map);
165
+
166
+        $object['type'] = $type_map[$this->obj->type()];
167
+
168
+        // read type-specific properties
169
+        switch ($object['type']) {
170
+        case 'dictionary':
171
+            $dict = $this->obj->dictionary();
172
+            $object['language'] = $dict->language();
173
+            $object['e'] = self::vector2array($dict->entries());
174
+            break;
175
+
176
+        case 'category':
177
+            // TODO: implement this
178
+            break;
179
+
180
+        case 'file_driver':
181
+            $driver = $this->obj->fileDriver();
182
+
183
+            $object['driver']  = $driver->driver();
184
+            $object['title']   = $driver->title();
185
+            $object['enabled'] = $driver->enabled();
186
+
187
+            foreach ($this->driver_settings_fields as $field) {
188
+                $object[$field] = $driver->{$field}();
189
+            }
190
+
191
+            break;
192
+
193
+        case 'relation':
194
+            $relation = $this->obj->relation();
195
+
196
+            $object['name']     = $relation->name();
197
+            $object['category'] = $relation->type();
198
+            $object['color']    = $relation->color();
199
+            $object['parent']   = $relation->parent();
200
+            $object['iconName'] = $relation->iconName();
201
+            $object['priority'] = $relation->priority();
202
+            $object['members']  = self::vector2array($relation->members());
203
+
204
+            break;
205
+
206
+        case 'snippet':
207
+            $collection = $this->obj->snippets();
208
+
209
+            $object['name']     = $collection->name();
210
+            $object['snippets'] = array();
211
+
212
+            $snippets = $collection->snippets();
213
+            for ($i=0; $i < $snippets->size(); $i++) {
214
+                $snippet = $snippets->get($i);
215
+                $object['snippets'][] = array(
216
+                    'name'     => $snippet->name(),
217
+                    'text'     => $snippet->text(),
218
+                    'type'     => $snippet->textType() == Snippet::HTML ? 'html' : 'plain',
219
+                    'shortcut' => $snippet->shortCut(),
220
+                );
221
+            }
222
+
223
+            break;
224
+        }
225
+
226
+        // adjust content-type string
227
+        if ($object['type']) {
228
+            $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type'];
229
+        }
230
+
231
+        $this->data = $object;
232
+        return $this->data;
233
+    }
234
+
235
+    /**
236
+     * Callback for kolab_storage_cache to get object specific tags to cache
237
+     *
238
+     * @return array List of tags to save in cache
239
+     */
240
+    public function get_tags()
241
+    {
242
+        $tags = array();
243
+
244
+        switch ($this->data['type']) {
245
+        case 'dictionary':
246
+            $tags = array($this->data['language']);
247
+            break;
248
+
249
+        case 'relation':
250
+            $tags = array('category:' . $this->data['category']);
251
+            break;
252
+        }
253
+
254
+        return $tags;
255
+    }
256
+
257
+    /**
258
+     * Callback for kolab_storage_cache to get words to index for fulltext search
259
+     *
260
+     * @return array List of words to save in cache
261
+     */
262
+    public function get_words()
263
+    {
264
+        $words = array();
265
+
266
+        foreach ((array)$this->data['members'] as $url) {
267
+            $member = kolab_storage_config::parse_member_url($url);
268
+
269
+            if (empty($member)) {
270
+                if (strpos($url, 'urn:uuid:') === 0) {
271
+                    $words[] = substr($url, 9);
272
+                }
273
+            }
274
+            else if (!empty($member['params']['message-id'])) {
275
+                $words[] = $member['params']['message-id'];
276
+            }
277
+            else {
278
+                // derive message identifier from URI
279
+                $words[] = md5($url);
280
+            }
281
+        }
282
+
283
+        return $words;
284
+    }
285
+}
286
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_format_contact.php Added
484
 
1
@@ -0,0 +1,482 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab Contact model class
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+
26
+class kolab_format_contact extends kolab_format
27
+{
28
+    public $CTYPE = 'application/vcard+xml';
29
+    public $CTYPEv2 = 'application/x-vnd.kolab.contact';
30
+
31
+    protected $objclass = 'Contact';
32
+    protected $read_func = 'readContact';
33
+    protected $write_func = 'writeContact';
34
+
35
+    public static $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'email:address');
36
+
37
+    public $phonetypes = array(
38
+        'home'    => Telephone::Home,
39
+        'work'    => Telephone::Work,
40
+        'text'    => Telephone::Text,
41
+        'main'    => Telephone::Voice,
42
+        'homefax' => Telephone::Fax,
43
+        'workfax' => Telephone::Fax,
44
+        'mobile'  => Telephone::Cell,
45
+        'video'   => Telephone::Video,
46
+        'pager'   => Telephone::Pager,
47
+        'car'     => Telephone::Car,
48
+        'other'   => Telephone::Textphone,
49
+    );
50
+
51
+    public $emailtypes = array(
52
+        'home' => Email::Home,
53
+        'work' => Email::Work,
54
+        'other' => Email::NoType,
55
+    );
56
+
57
+    public $addresstypes = array(
58
+        'home' => Address::Home,
59
+        'work' => Address::Work,
60
+        'office' => 0,
61
+    );
62
+
63
+    private $gendermap = array(
64
+        'female' => Contact::Female,
65
+        'male'   => Contact::Male,
66
+    );
67
+
68
+    private $relatedmap = array(
69
+        'manager'   => Related::Manager,
70
+        'assistant' => Related::Assistant,
71
+        'spouse'    => Related::Spouse,
72
+        'children'  => Related::Child,
73
+    );
74
+
75
+
76
+    /**
77
+     * Default constructor
78
+     */
79
+    function __construct($xmldata = null, $version = 3.0)
80
+    {
81
+        parent::__construct($xmldata, $version);
82
+
83
+        // complete phone types
84
+        $this->phonetypes['homefax'] |= Telephone::Home;
85
+        $this->phonetypes['workfax'] |= Telephone::Work;
86
+    }
87
+
88
+    /**
89
+     * Set contact properties to the kolabformat object
90
+     *
91
+     * @param array  Contact data as hash array
92
+     */
93
+    public function set(&$object)
94
+    {
95
+        // set common object properties
96
+        parent::set($object);
97
+
98
+        // do the hard work of setting object values
99
+        $nc = new NameComponents;
100
+        $nc->setSurnames(self::array2vector($object['surname']));
101
+        $nc->setGiven(self::array2vector($object['firstname']));
102
+        $nc->setAdditional(self::array2vector($object['middlename']));
103
+        $nc->setPrefixes(self::array2vector($object['prefix']));
104
+        $nc->setSuffixes(self::array2vector($object['suffix']));
105
+        $this->obj->setNameComponents($nc);
106
+        $this->obj->setName($object['name']);
107
+        $this->obj->setCategories(self::array2vector($object['categories']));
108
+
109
+        if (isset($object['nickname']))
110
+            $this->obj->setNickNames(self::array2vector($object['nickname']));
111
+        if (isset($object['jobtitle']))
112
+            $this->obj->setTitles(self::array2vector($object['jobtitle']));
113
+
114
+        // organisation related properties (affiliation)
115
+        $org = new Affiliation;
116
+        $offices = new vectoraddress;
117
+        if ($object['organization'])
118
+            $org->setOrganisation($object['organization']);
119
+        if ($object['department'])
120
+            $org->setOrganisationalUnits(self::array2vector($object['department']));
121
+        if ($object['profession'])
122
+            $org->setRoles(self::array2vector($object['profession']));
123
+
124
+        $rels = new vectorrelated;
125
+        foreach (array('manager','assistant') as $field) {
126
+            if (!empty($object[$field])) {
127
+                $reltype = $this->relatedmap[$field];
128
+                foreach ((array)$object[$field] as $value) {
129
+                    $rels->push(new Related(Related::Text, $value, $reltype));
130
+                }
131
+            }
132
+        }
133
+        $org->setRelateds($rels);
134
+
135
+        // im, email, url
136
+        $this->obj->setIMaddresses(self::array2vector($object['im']));
137
+
138
+        if (class_exists('vectoremail')) {
139
+            $vemails = new vectoremail;
140
+            foreach ((array)$object['email'] as $email) {
141
+                $type = $this->emailtypes[$email['type']];
142
+                $vemails->push(new Email($email['address'], intval($type)));
143
+            }
144
+        }
145
+        else {
146
+            $vemails = self::array2vector(array_map(function($v){ return $v['address']; }, $object['email']));
147
+        }
148
+        $this->obj->setEmailAddresses($vemails);
149
+
150
+        $vurls = new vectorurl;
151
+        foreach ((array)$object['website'] as $url) {
152
+            $type = $url['type'] == 'blog' ? Url::Blog : Url::NoType;
153
+            $vurls->push(new Url($url['url'], $type));
154
+        }
155
+        $this->obj->setUrls($vurls);
156
+
157
+        // addresses
158
+        $adrs = new vectoraddress;
159
+        foreach ((array)$object['address'] as $address) {
160
+            $adr = new Address;
161
+            $type = $this->addresstypes[$address['type']];
162
+            if (isset($type))
163
+                $adr->setTypes($type);
164
+            else if ($address['type'])
165
+                $adr->setLabel($address['type']);
166
+            if ($address['street'])
167
+                $adr->setStreet($address['street']);
168
+            if ($address['locality'])
169
+                $adr->setLocality($address['locality']);
170
+            if ($address['code'])
171
+                $adr->setCode($address['code']);
172
+            if ($address['region'])
173
+                $adr->setRegion($address['region']);
174
+            if ($address['country'])
175
+                $adr->setCountry($address['country']);
176
+
177
+            if ($address['type'] == 'office')
178
+                $offices->push($adr);
179
+            else
180
+                $adrs->push($adr);
181
+        }
182
+        $this->obj->setAddresses($adrs);
183
+        $org->setAddresses($offices);
184
+
185
+        // add org affiliation after addresses are set
186
+        $orgs = new vectoraffiliation;
187
+        $orgs->push($org);
188
+        $this->obj->setAffiliations($orgs);
189
+
190
+        // telephones
191
+        $tels = new vectortelephone;
192
+        foreach ((array)$object['phone'] as $phone) {
193
+            $tel = new Telephone;
194
+            if (isset($this->phonetypes[$phone['type']]))
195
+                $tel->setTypes($this->phonetypes[$phone['type']]);
196
+            $tel->setNumber($phone['number']);
197
+            $tels->push($tel);
198
+        }
199
+        $this->obj->setTelephones($tels);
200
+
201
+        if (isset($object['gender']))
202
+            $this->obj->setGender($this->gendermap[$object['gender']] ? $this->gendermap[$object['gender']] : Contact::NotSet);
203
+        if (isset($object['notes']))
204
+            $this->obj->setNote($object['notes']);
205
+        if (isset($object['freebusyurl']))
206
+            $this->obj->setFreeBusyUrl($object['freebusyurl']);
207
+        if (isset($object['lang']))
208
+            $this->obj->setLanguages(self::array2vector($object['lang']));
209
+        if (isset($object['birthday']))
210
+            $this->obj->setBDay(self::get_datetime($object['birthday'], false, true));
211
+        if (isset($object['anniversary']))
212
+            $this->obj->setAnniversary(self::get_datetime($object['anniversary'], false, true));
213
+
214
+        if (!empty($object['photo'])) {
215
+            if ($type = rcube_mime::image_content_type($object['photo']))
216
+                $this->obj->setPhoto($object['photo'], $type);
217
+        }
218
+        else if (isset($object['photo']))
219
+            $this->obj->setPhoto('','');
220
+        else if ($this->obj->photoMimetype())  // load saved photo for caching
221
+            $object['photo'] = $this->obj->photo();
222
+
223
+        // spouse and children are relateds
224
+        $rels = new vectorrelated;
225
+        foreach (array('spouse','children') as $field) {
226
+            if (!empty($object[$field])) {
227
+                $reltype = $this->relatedmap[$field];
228
+                foreach ((array)$object[$field] as $value) {
229
+                    $rels->push(new Related(Related::Text, $value, $reltype));
230
+                }
231
+            }
232
+        }
233
+        // add other relateds
234
+        if (is_array($object['related'])) {
235
+            foreach ($object['related'] as $value) {
236
+                $rels->push(new Related(Related::Text, $value));
237
+            }
238
+        }
239
+        $this->obj->setRelateds($rels);
240
+
241
+        // insert/replace crypto keys
242
+        $pgp_index = $pkcs7_index = -1;
243
+        $keys = $this->obj->keys();
244
+        for ($i=0; $i < $keys->size(); $i++) {
245
+            $key = $keys->get($i);
246
+            if ($pgp_index < 0 && $key->type() == Key::PGP)
247
+                $pgp_index = $i;
248
+            else if ($pkcs7_index < 0 && $key->type() == Key::PKCS7_MIME)
249
+                $pkcs7_index = $i;
250
+        }
251
+
252
+        $pgpkey   = $object['pgppublickey']   ? new Key($object['pgppublickey'], Key::PGP) : new Key();
253
+        $pkcs7key = $object['pkcs7publickey'] ? new Key($object['pkcs7publickey'], Key::PKCS7_MIME) : new Key();
254
+
255
+        if ($pgp_index >= 0)
256
+            $keys->set($pgp_index, $pgpkey);
257
+        else if (!empty($object['pgppublickey']))
258
+            $keys->push($pgpkey);
259
+        if ($pkcs7_index >= 0)
260
+            $keys->set($pkcs7_index, $pkcs7key);
261
+        else if (!empty($object['pkcs7publickey']))
262
+            $keys->push($pkcs7key);
263
+
264
+        $this->obj->setKeys($keys);
265
+
266
+        // TODO: handle language, gpslocation, etc.
267
+
268
+        // set type property for proper caching
269
+        $object['_type'] = 'contact';
270
+
271
+        // cache this data
272
+        $this->data = $object;
273
+        unset($this->data['_formatobj']);
274
+    }
275
+
276
+    /**
277
+     *
278
+     */
279
+    public function is_valid()
280
+    {
281
+        return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/));
282
+    }
283
+
284
+    /**
285
+     * Convert the Contact object into a hash array data structure
286
+     *
287
+     * @param array Additional data for merge
288
+     *
289
+     * @return array  Contact data as hash array
290
+     */
291
+    public function to_array($data = array())
292
+    {
293
+        // return cached result
294
+        if (!empty($this->data))
295
+            return $this->data;
296
+
297
+        // read common object props into local data object
298
+        $object = parent::to_array($data);
299
+
300
+        $object['name'] = $this->obj->name();
301
+
302
+        $nc = $this->obj->nameComponents();
303
+        $object['surname']    = join(' ', self::vector2array($nc->surnames()));
304
+        $object['firstname']  = join(' ', self::vector2array($nc->given()));
305
+        $object['middlename'] = join(' ', self::vector2array($nc->additional()));
306
+        $object['prefix']     = join(' ', self::vector2array($nc->prefixes()));
307
+        $object['suffix']     = join(' ', self::vector2array($nc->suffixes()));
308
+        $object['nickname']   = join(' ', self::vector2array($this->obj->nickNames()));
309
+        $object['jobtitle']   = join(' ', self::vector2array($this->obj->titles()));
310
+        $object['categories'] = self::vector2array($this->obj->categories());
311
+
312
+        // organisation related properties (affiliation)
313
+        $orgs = $this->obj->affiliations();
314
+        if ($orgs->size()) {
315
+            $org = $orgs->get(0);
316
+            $object['organization']   = $org->organisation();
317
+            $object['profession']     = join(' ', self::vector2array($org->roles()));
318
+            $object['department']     = join(' ', self::vector2array($org->organisationalUnits()));
319
+            $this->read_relateds($org->relateds(), $object);
320
+        }
321
+
322
+        $object['im'] = self::vector2array($this->obj->imAddresses());
323
+
324
+        $emails = $this->obj->emailAddresses();
325
+        if ($emails instanceof vectoremail) {
326
+            $emailtypes = array_flip($this->emailtypes);
327
+            for ($i=0; $i < $emails->size(); $i++) {
328
+                $email = $emails->get($i);
329
+                $object['email'][] = array('address' => $email->address(), 'type' => $emailtypes[$email->types()]);
330
+            }
331
+        }
332
+        else {
333
+            $object['email'] = self::vector2array($emails);
334
+        }
335
+
336
+        $urls = $this->obj->urls();
337
+        for ($i=0; $i < $urls->size(); $i++) {
338
+            $url = $urls->get($i);
339
+            $subtype = $url->type() == Url::Blog ? 'blog' : 'homepage';
340
+            $object['website'][] = array('url' => $url->url(), 'type' => $subtype);
341
+        }
342
+
343
+        // addresses
344
+        $this->read_addresses($this->obj->addresses(), $object);
345
+        if ($org && ($offices = $org->addresses()))
346
+            $this->read_addresses($offices, $object, 'office');
347
+
348
+        // telehones
349
+        $tels = $this->obj->telephones();
350
+        $teltypes = array_flip($this->phonetypes);
351
+        for ($i=0; $i < $tels->size(); $i++) {
352
+            $tel = $tels->get($i);
353
+            $object['phone'][] = array('number' => $tel->number(), 'type' => $teltypes[$tel->types()]);
354
+        }
355
+
356
+        $object['notes'] = $this->obj->note();
357
+        $object['freebusyurl'] = $this->obj->freeBusyUrl();
358
+        $object['lang'] = self::vector2array($this->obj->languages());
359
+
360
+        if ($bday = self::php_datetime($this->obj->bDay()))
361
+            $object['birthday'] = $bday;
362
+
363
+        if ($anniversary = self::php_datetime($this->obj->anniversary()))
364
+            $object['anniversary'] = $anniversary;
365
+
366
+        $gendermap = array_flip($this->gendermap);
367
+        if (($g = $this->obj->gender()) && $gendermap[$g])
368
+            $object['gender'] = $gendermap[$g];
369
+
370
+        if ($this->obj->photoMimetype())
371
+            $object['photo'] = $this->obj->photo();
372
+        else if ($this->xmlobject && ($photo_name = $this->xmlobject->pictureAttachmentName()))
373
+            $object['photo'] = $photo_name;
374
+
375
+        // relateds -> spouse, children
376
+        $this->read_relateds($this->obj->relateds(), $object, 'related');
377
+
378
+        // crypto settings: currently only key values are supported
379
+        $keys = $this->obj->keys();
380
+        for ($i=0; is_object($keys) && $i < $keys->size(); $i++) {
381
+            $key = $keys->get($i);
382
+            if ($key->type() == Key::PGP)
383
+                $object['pgppublickey'] = $key->key();
384
+            else if ($key->type() == Key::PKCS7_MIME)
385
+                $object['pkcs7publickey'] = $key->key();
386
+        }
387
+
388
+        $this->data = $object;
389
+        return $this->data;
390
+    }
391
+
392
+    /**
393
+     * Callback for kolab_storage_cache to get words to index for fulltext search
394
+     *
395
+     * @return array List of words to save in cache
396
+     */
397
+    public function get_words()
398
+    {
399
+        $data = '';
400
+        foreach (self::$fulltext_cols as $colname) {
401
+            list($col, $field) = explode(':', $colname);
402
+
403
+            if ($field) {
404
+                $a = array();
405
+                foreach ((array)$this->data[$col] as $attr)
406
+                    $a[] = $attr[$field];
407
+                $val = join(' ', $a);
408
+            }
409
+            else {
410
+                $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
411
+            }
412
+
413
+            if (strlen($val))
414
+                $data .= $val . ' ';
415
+        }
416
+
417
+        return array_unique(rcube_utils::normalize_string($data, true));
418
+    }
419
+
420
+    /**
421
+     * Callback for kolab_storage_cache to get object specific tags to cache
422
+     *
423
+     * @return array List of tags to save in cache
424
+     */
425
+    public function get_tags()
426
+    {
427
+        $tags = array();
428
+
429
+        if (!empty($this->data['birthday'])) {
430
+            $tags[] = 'x-has-birthday';
431
+        }
432
+
433
+        return $tags;
434
+    }
435
+
436
+    /**
437
+     * Helper method to copy contents of an Address vector to the contact data object
438
+     */
439
+    private function read_addresses($addresses, &$object, $type = null)
440
+    {
441
+        $adrtypes = array_flip($this->addresstypes);
442
+
443
+        for ($i=0; $i < $addresses->size(); $i++) {
444
+            $adr = $addresses->get($i);
445
+            $object['address'][] = array(
446
+                'type'     => $type ? $type : ($adrtypes[$adr->types()] ? $adrtypes[$adr->types()] : ''), /*$adr->label()),*/
447
+                'street'   => $adr->street(),
448
+                'code'     => $adr->code(),
449
+                'locality' => $adr->locality(),
450
+                'region'   => $adr->region(),
451
+                'country'  => $adr->country()
452
+            );
453
+        }
454
+    }
455
+
456
+    /**
457
+     * Helper method to map contents of a Related vector to the contact data object
458
+     */
459
+    private function read_relateds($rels, &$object, $catchall = null)
460
+    {
461
+        $typemap = array_flip($this->relatedmap);
462
+
463
+        for ($i=0; $i < $rels->size(); $i++) {
464
+            $rel = $rels->get($i);
465
+            if ($rel->type() != Related::Text)  // we can't handle UID relations yet
466
+                continue;
467
+
468
+            $known = false;
469
+            $types = $rel->relationTypes();
470
+            foreach ($typemap as $t => $field) {
471
+                if ($types & $t) {
472
+                    $object[$field][] = $rel->text();
473
+                    $known = true;
474
+                    break;
475
+                }
476
+            }
477
+
478
+            if (!$known && $catchall) {
479
+                $object[$catchall][] = $rel->text();
480
+            }
481
+        }
482
+    }
483
+}
484
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php Added
127
 
1
@@ -0,0 +1,125 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab Distribution List model class
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+
26
+class kolab_format_distributionlist extends kolab_format
27
+{
28
+    public $CTYPE = 'application/vcard+xml';
29
+    public $CTYPEv2 = 'application/x-vnd.kolab.distribution-list';
30
+
31
+    protected $objclass = 'DistList';
32
+    protected $read_func = 'readDistlist';
33
+    protected $write_func = 'writeDistlist';
34
+
35
+
36
+    /**
37
+     * Set properties to the kolabformat object
38
+     *
39
+     * @param array  Object data as hash array
40
+     */
41
+    public function set(&$object)
42
+    {
43
+        // set common object properties
44
+        parent::set($object);
45
+
46
+        $this->obj->setName($object['name']);
47
+
48
+        $seen = array();
49
+        $members = new vectorcontactref;
50
+        foreach ((array)$object['member'] as $i => $member) {
51
+            if ($member['uid']) {
52
+                $key = 'uid:' . $member['uid'];
53
+                $m = new ContactReference(ContactReference::UidReference, $member['uid']);
54
+            }
55
+            else if ($member['email']) {
56
+                $key = 'mailto:' . $member['email'];
57
+                $m = new ContactReference(ContactReference::EmailReference, $member['email']);
58
+                $m->setName($member['name']);
59
+            }
60
+            else {
61
+                continue;
62
+            }
63
+
64
+            if (!$seen[$key]++) {
65
+                $members->push($m);
66
+            }
67
+            else {
68
+                // remove dupes for caching
69
+                unset($object['member'][$i]);
70
+            }
71
+        }
72
+
73
+        $this->obj->setMembers($members);
74
+
75
+        // set type property for proper caching
76
+        $object['_type'] = 'distribution-list';
77
+
78
+        // cache this data
79
+        $this->data = $object;
80
+        unset($this->data['_formatobj']);
81
+    }
82
+
83
+    public function is_valid()
84
+    {
85
+        return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
86
+    }
87
+
88
+    /**
89
+     * Convert the Distlist object into a hash array data structure
90
+     *
91
+     * @param array Additional data for merge
92
+     *
93
+     * @return array  Distribution list data as hash array
94
+     */
95
+    public function to_array($data = array())
96
+    {
97
+        // return cached result
98
+        if (!empty($this->data))
99
+            return $this->data;
100
+
101
+        // read common object props into local data object
102
+        $object = parent::to_array($data);
103
+
104
+        // add object properties
105
+        $object += array(
106
+            'name'      => $this->obj->name(),
107
+            'member'    => array(),
108
+            '_type'     => 'distribution-list',
109
+        );
110
+
111
+        $members = $this->obj->members();
112
+        for ($i=0; $i < $members->size(); $i++) {
113
+            $member = $members->get($i);
114
+//            if ($member->type() == ContactReference::UidReference && ($uid = $member->uid()))
115
+                $object['member'][] = array(
116
+                    'uid'   => $member->uid(),
117
+                    'email' => $member->email(),
118
+                    'name'  => $member->name(),
119
+                );
120
+        }
121
+
122
+        $this->data = $object;
123
+        return $this->data;
124
+    }
125
+
126
+}
127
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_format_event.php Added
246
 
1
@@ -0,0 +1,244 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab Event model class
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+
26
+class kolab_format_event extends kolab_format_xcal
27
+{
28
+    public $CTYPEv2 = 'application/x-vnd.kolab.event';
29
+
30
+    public $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
31
+
32
+    protected $objclass = 'Event';
33
+    protected $read_func = 'readEvent';
34
+    protected $write_func = 'writeEvent';
35
+
36
+    /**
37
+     * Default constructor
38
+     */
39
+    function __construct($data = null, $version = 3.0)
40
+    {
41
+        parent::__construct(is_string($data) ? $data : null, $version);
42
+
43
+        // got an Event object as argument
44
+        if (is_object($data) && is_a($data, $this->objclass)) {
45
+            $this->obj = $data;
46
+            $this->loaded = true;
47
+        }
48
+    }
49
+
50
+    /**
51
+     * Clones into an instance of libcalendaring's extended EventCal class
52
+     *
53
+     * @return mixed EventCal object or false on failure
54
+     */
55
+    public function to_libcal()
56
+    {
57
+        static $error_logged = false;
58
+
59
+        if (class_exists('kolabcalendaring')) {
60
+            return new EventCal($this->obj);
61
+        }
62
+        else if (!$error_logged) {
63
+            $error_logged = true;
64
+            rcube::raise_error(array(
65
+                'code' => 900, 'type' => 'php',
66
+                'message' => "required kolabcalendaring module not found"
67
+            ), true);
68
+        }
69
+
70
+        return false;
71
+    }
72
+
73
+    /**
74
+     * Set event properties to the kolabformat object
75
+     *
76
+     * @param array  Event data as hash array
77
+     */
78
+    public function set(&$object)
79
+    {
80
+        // set common xcal properties
81
+        parent::set($object);
82
+
83
+        // do the hard work of setting object values
84
+        $this->obj->setStart(self::get_datetime($object['start'], null, $object['allday']));
85
+        $this->obj->setEnd(self::get_datetime($object['end'], null, $object['allday']));
86
+        $this->obj->setTransparency($object['free_busy'] == 'free');
87
+
88
+        $status = kolabformat::StatusUndefined;
89
+        if ($object['free_busy'] == 'tentative')
90
+            $status = kolabformat::StatusTentative;
91
+        if ($object['cancelled'])
92
+            $status = kolabformat::StatusCancelled;
93
+        else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
94
+            $status = $this->status_map[$object['status']];
95
+        $this->obj->setStatus($status);
96
+
97
+        // save recurrence exceptions
98
+        if (is_array($object['recurrence']) && $object['recurrence']['EXCEPTIONS']) {
99
+            $vexceptions = new vectorevent;
100
+            foreach((array)$object['recurrence']['EXCEPTIONS'] as $i => $exception) {
101
+                $exevent = new kolab_format_event;
102
+                $exevent->set(($compacted = $this->compact_exception($exception, $object)));  // only save differing values
103
+                $exevent->obj->setRecurrenceID(self::get_datetime($exception['start'], null, true), (bool)$exception['thisandfuture']);
104
+                $vexceptions->push($exevent->obj);
105
+                // write cleaned-up exception data back to memory/cache
106
+                $object['recurrence']['EXCEPTIONS'][$i] = $this->expand_exception($compacted, $object);
107
+            }
108
+            $this->obj->setExceptions($vexceptions);
109
+        }
110
+
111
+        // cache this data
112
+        $this->data = $object;
113
+        unset($this->data['_formatobj']);
114
+    }
115
+
116
+    /**
117
+     *
118
+     */
119
+    public function is_valid()
120
+    {
121
+        return !$this->formaterror && (($this->data && !empty($this->data['start']) && !empty($this->data['end'])) ||
122
+            (is_object($this->obj) && $this->obj->isValid() && $this->obj->uid()));
123
+    }
124
+
125
+    /**
126
+     * Convert the Event object into a hash array data structure
127
+     *
128
+     * @param array Additional data for merge
129
+     *
130
+     * @return array  Event data as hash array
131
+     */
132
+    public function to_array($data = array())
133
+    {
134
+        // return cached result
135
+        if (!empty($this->data))
136
+            return $this->data;
137
+
138
+        // read common xcal props
139
+        $object = parent::to_array($data);
140
+
141
+        // read object properties
142
+        $object += array(
143
+            'end'         => self::php_datetime($this->obj->end()),
144
+            'allday'      => $this->obj->start()->isDateOnly(),
145
+            'free_busy'   => $this->obj->transparency() ? 'free' : 'busy',  // TODO: transparency is only boolean
146
+            'attendees'   => array(),
147
+        );
148
+
149
+        // derive event end from duration (#1916)
150
+        if (!$object['end'] && $object['start'] && ($duration = $this->obj->duration()) && $duration->isValid()) {
151
+            $interval = new DateInterval('PT0S');
152
+            $interval->d = $duration->weeks() * 7 + $duration->days();
153
+            $interval->h = $duration->hours();
154
+            $interval->i = $duration->minutes();
155
+            $interval->s = $duration->seconds();
156
+            $object['end'] = clone $object['start'];
157
+            $object['end']->add($interval);
158
+        }
159
+
160
+        // organizer is part of the attendees list in Roundcube
161
+        if ($object['organizer']) {
162
+            $object['organizer']['role'] = 'ORGANIZER';
163
+            array_unshift($object['attendees'], $object['organizer']);
164
+        }
165
+
166
+        // status defines different event properties...
167
+        $status = $this->obj->status();
168
+        if ($status == kolabformat::StatusTentative)
169
+          $object['free_busy'] = 'tentative';
170
+        else if ($status == kolabformat::StatusCancelled)
171
+          $object['cancelled'] = true;
172
+
173
+        // this is an exception object
174
+        if ($this->obj->recurrenceID()->isValid()) {
175
+            $object['thisandfuture'] = $this->obj->thisAndFuture();
176
+        }
177
+        // read exception event objects
178
+        else if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) {
179
+            $recurrence_exceptions = array();
180
+            for ($i=0; $i < $exceptions->size(); $i++) {
181
+                if (($exobj = $exceptions->get($i))) {
182
+                    $exception = new kolab_format_event($exobj);
183
+                    if ($exception->is_valid()) {
184
+                        $recurrence_exceptions[] = $this->expand_exception($exception->to_array(), $object);
185
+                    }
186
+                }
187
+            }
188
+            $object['recurrence']['EXCEPTIONS'] = $recurrence_exceptions;
189
+        }
190
+
191
+        return $this->data = $object;
192
+    }
193
+
194
+    /**
195
+     * Callback for kolab_storage_cache to get object specific tags to cache
196
+     *
197
+     * @return array List of tags to save in cache
198
+     */
199
+    public function get_tags()
200
+    {
201
+        $tags = parent::get_tags();
202
+
203
+        foreach ((array)$this->data['categories'] as $cat) {
204
+            $tags[] = rcube_utils::normalize_string($cat);
205
+        }
206
+
207
+        return $tags;
208
+    }
209
+
210
+    /**
211
+     * Remove some attributes from the exception container
212
+     */
213
+    private function compact_exception($exception, $master)
214
+    {
215
+      $forbidden = array('recurrence','organizer','attendees','sequence');
216
+
217
+      foreach ($forbidden as $prop) {
218
+        if (array_key_exists($prop, $exception)) {
219
+          unset($exception[$prop]);
220
+        }
221
+      }
222
+
223
+      foreach ($master as $prop => $value) {
224
+        if (isset($exception[$prop]) && gettype($exception[$prop]) == gettype($value) && $exception[$prop] == $value) {
225
+          unset($exception[$prop]);
226
+        }
227
+      }
228
+
229
+      return $exception;
230
+    }
231
+
232
+    /**
233
+     * Copy attributes not specified by the exception from the master event
234
+     */
235
+    private function expand_exception($exception, $master)
236
+    {
237
+      foreach ($master as $prop => $value) {
238
+        if (empty($exception[$prop]) && !empty($value))
239
+          $exception[$prop] = $value;
240
+      }
241
+
242
+      return $exception;
243
+    }
244
+
245
+}
246
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_format_file.php Added
158
 
1
@@ -0,0 +1,156 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab File model class
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ * @author Aleksander Machniak <machniak@kolabsys.com>
10
+ *
11
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
12
+ *
13
+ * This program is free software: you can redistribute it and/or modify
14
+ * it under the terms of the GNU Affero General Public License as
15
+ * published by the Free Software Foundation, either version 3 of the
16
+ * License, or (at your option) any later version.
17
+ *
18
+ * This program is distributed in the hope that it will be useful,
19
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
+ * GNU Affero General Public License for more details.
22
+ *
23
+ * You should have received a copy of the GNU Affero General Public License
24
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
25
+ */
26
+
27
+class kolab_format_file extends kolab_format
28
+{
29
+    public $CTYPE = 'application/vnd.kolab+xml';
30
+
31
+    protected $objclass = 'File';
32
+    protected $read_func = 'kolabformat::readKolabFile';
33
+    protected $write_func = 'kolabformat::writeKolabFile';
34
+
35
+    protected $sensitivity_map = array(
36
+        'public'       => kolabformat::ClassPublic,
37
+        'private'      => kolabformat::ClassPrivate,
38
+        'confidential' => kolabformat::ClassConfidential,
39
+    );
40
+
41
+    /**
42
+     * Set properties to the kolabformat object
43
+     *
44
+     * @param array  Object data as hash array
45
+     */
46
+    public function set(&$object)
47
+    {
48
+        // set common object properties
49
+        parent::set($object);
50
+
51
+        $this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
52
+        $this->obj->setCategories(self::array2vector($object['categories']));
53
+
54
+        if (isset($object['notes'])) {
55
+            $this->obj->setNote($object['notes']);
56
+        }
57
+
58
+        // Add file attachment
59
+        if (!empty($object['_attachments'])) {
60
+            $cid         = key($object['_attachments']);
61
+            $attach_attr = $object['_attachments'][$cid];
62
+            $attach      = new Attachment;
63
+
64
+            $attach->setLabel((string)$attach_attr['name']);
65
+            $attach->setUri('cid:' . $cid, $attach_attr['mimetype']);
66
+            $this->obj->setFile($attach);
67
+
68
+            // make sure size is set, so object saved in cache contains this info
69
+            if (!isset($attach_attr['size'])) {
70
+                $size = 0;
71
+
72
+                if (!empty($attach_attr['content'])) {
73
+                    if (is_resource($attach_attr['content'])) {
74
+                        $stat = fstat($attach_attr['content']);
75
+                        $size = $stat ? $stat['size'] : 0;
76
+                    }
77
+                    else {
78
+                        $size = strlen($attach_attr['content']);
79
+                    }
80
+                }
81
+                else if (isset($attach_attr['path'])) {
82
+                    $size = @filesize($attach_attr['path']);
83
+                }
84
+
85
+                $object['_attachments'][$cid]['size'] = $size;
86
+            }
87
+        }
88
+
89
+        // cache this data
90
+        $this->data = $object;
91
+        unset($this->data['_formatobj']);
92
+    }
93
+
94
+    /**
95
+     * Check if object's data validity
96
+     */
97
+    public function is_valid()
98
+    {
99
+        return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
100
+    }
101
+
102
+    /**
103
+     * Convert the Configuration object into a hash array data structure
104
+     *
105
+     * @param array Additional data for merge
106
+     *
107
+     * @return array  Config object data as hash array
108
+     */
109
+    public function to_array($data = array())
110
+    {
111
+        // return cached result
112
+        if (!empty($this->data)) {
113
+            return $this->data;
114
+        }
115
+
116
+        // read common object props into local data object
117
+        $object = parent::to_array($data);
118
+
119
+        $sensitivity_map = array_flip($this->sensitivity_map);
120
+
121
+        // read object properties
122
+        $object += array(
123
+            'sensitivity' => $sensitivity_map[$this->obj->classification()],
124
+            'categories'  => self::vector2array($this->obj->categories()),
125
+            'notes'       => $this->obj->note(),
126
+        );
127
+
128
+        return $this->data = $object;
129
+    }
130
+
131
+    /**
132
+     * Callback for kolab_storage_cache to get object specific tags to cache
133
+     *
134
+     * @return array List of tags to save in cache
135
+     */
136
+    public function get_tags()
137
+    {
138
+        $tags = array();
139
+
140
+        foreach ((array)$this->data['categories'] as $cat) {
141
+            $tags[] = rcube_utils::normalize_string($cat);
142
+        }
143
+
144
+        // Add file mimetype to tags
145
+        if (!empty($this->data['_attachments'])) {
146
+            reset($this->data['_attachments']);
147
+            $key        = key($this->data['_attachments']);
148
+            $attachment = $this->data['_attachments'][$key];
149
+
150
+            if ($attachment['mimetype']) {
151
+                $tags[] = $attachment['mimetype'];
152
+            }
153
+        }
154
+
155
+        return $tags;
156
+    }
157
+}
158
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_format_journal.php Added
84
 
1
@@ -0,0 +1,82 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab Journal model class
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+
26
+class kolab_format_journal extends kolab_format
27
+{
28
+    public $CTYPE = 'application/calendar+xml';
29
+    public $CTYPEv2 = 'application/x-vnd.kolab.journal';
30
+
31
+    protected $objclass = 'Journal';
32
+    protected $read_func = 'readJournal';
33
+    protected $write_func = 'writeJournal';
34
+
35
+
36
+    /**
37
+     * Set properties to the kolabformat object
38
+     *
39
+     * @param array  Object data as hash array
40
+     */
41
+    public function set(&$object)
42
+    {
43
+        // set common object properties
44
+        parent::set($object);
45
+
46
+        // TODO: set object propeties
47
+
48
+        // cache this data
49
+        $this->data = $object;
50
+        unset($this->data['_formatobj']);
51
+    }
52
+
53
+    /**
54
+     *
55
+     */
56
+    public function is_valid()
57
+    {
58
+        return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
59
+    }
60
+
61
+    /**
62
+     * Convert the Configuration object into a hash array data structure
63
+     *
64
+     * @param array Additional data for merge
65
+     *
66
+     * @return array  Config object data as hash array
67
+     */
68
+    public function to_array($data = array())
69
+    {
70
+        // return cached result
71
+        if (!empty($this->data))
72
+            return $this->data;
73
+
74
+        // read common object props into local data object
75
+        $object = parent::to_array($data);
76
+
77
+        // TODO: read object properties
78
+
79
+        $this->data = $object;
80
+        return $this->data;
81
+    }
82
+
83
+}
84
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_format_note.php Added
155
 
1
@@ -0,0 +1,153 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab Note model class
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+
26
+class kolab_format_note extends kolab_format
27
+{
28
+    public $CTYPE = 'application/vnd.kolab+xml';
29
+    public $CTYPEv2 = 'application/x-vnd.kolab.note';
30
+
31
+    public static $fulltext_cols = array('title', 'description', 'categories');
32
+
33
+    protected $objclass = 'Note';
34
+    protected $read_func = 'readNote';
35
+    protected $write_func = 'writeNote';
36
+
37
+    protected $sensitivity_map = array(
38
+        'public'       => kolabformat::ClassPublic,
39
+        'private'      => kolabformat::ClassPrivate,
40
+        'confidential' => kolabformat::ClassConfidential,
41
+    );
42
+
43
+    /**
44
+     * Set properties to the kolabformat object
45
+     *
46
+     * @param array  Object data as hash array
47
+     */
48
+    public function set(&$object)
49
+    {
50
+        // set common object properties
51
+        parent::set($object);
52
+
53
+        $this->obj->setSummary($object['title']);
54
+        $this->obj->setDescription($object['description']);
55
+        $this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
56
+        $this->obj->setCategories(self::array2vector($object['categories']));
57
+
58
+        $this->set_attachments($object);
59
+
60
+        // cache this data
61
+        $this->data = $object;
62
+        unset($this->data['_formatobj']);
63
+    }
64
+
65
+    /**
66
+     *
67
+     */
68
+    public function is_valid()
69
+    {
70
+        return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
71
+    }
72
+
73
+    /**
74
+     * Convert the Configuration object into a hash array data structure
75
+     *
76
+     * @param array Additional data for merge
77
+     *
78
+     * @return array  Config object data as hash array
79
+     */
80
+    public function to_array($data = array())
81
+    {
82
+        // return cached result
83
+        if (!empty($this->data))
84
+            return $this->data;
85
+
86
+        // read common object props into local data object
87
+        $object = parent::to_array($data);
88
+
89
+        $sensitivity_map = array_flip($this->sensitivity_map);
90
+
91
+        // read object properties
92
+        $object += array(
93
+            'sensitivity' => $sensitivity_map[$this->obj->classification()],
94
+            'categories'  => self::vector2array($this->obj->categories()),
95
+            'title'       => $this->obj->summary(),
96
+            'description' => $this->obj->description(),
97
+        );
98
+
99
+        $this->get_attachments($object);
100
+
101
+        return $this->data = $object;
102
+    }
103
+
104
+    /**
105
+     * Callback for kolab_storage_cache to get object specific tags to cache
106
+     *
107
+     * @return array List of tags to save in cache
108
+     */
109
+    public function get_tags()
110
+    {
111
+        $tags = array();
112
+
113
+        foreach ((array)$this->data['categories'] as $cat) {
114
+            $tags[] = rcube_utils::normalize_string($cat);
115
+        }
116
+
117
+        // add tag for message references
118
+        foreach ((array)$this->data['links'] as $link) {
119
+            $url = parse_url($link);
120
+            if ($url['scheme'] == 'imap') {
121
+                parse_str($url['query'], $param);
122
+                $tags[] = 'ref:' . trim($param['message-id'] ?: urldecode($url['fragment']), '<> ');
123
+            }
124
+        }
125
+
126
+        return $tags;
127
+    }
128
+
129
+    /**
130
+     * Callback for kolab_storage_cache to get words to index for fulltext search
131
+     *
132
+     * @return array List of words to save in cache
133
+     */
134
+    public function get_words()
135
+    {
136
+        $data = '';
137
+        foreach (self::$fulltext_cols as $col) {
138
+            // convert HTML content to plain text
139
+            if ($col == 'description' && preg_match('/<(html|body)(\s[a-z]|>)/', $this->data[$col], $m) && strpos($this->data[$col], '</'.$m[1].'>')) {
140
+                $converter = new rcube_html2text($this->data[$col], false, false, 0);
141
+                $val = $converter->get_text();
142
+            }
143
+            else {
144
+                $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
145
+            }
146
+
147
+            if (strlen($val))
148
+                $data .= $val . ' ';
149
+        }
150
+
151
+        return array_filter(array_unique(rcube_utils::normalize_string($data, true)));
152
+    }
153
+
154
+}
155
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_format_task.php Added
131
 
1
@@ -0,0 +1,129 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab Task (ToDo) model class
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+
26
+class kolab_format_task extends kolab_format_xcal
27
+{
28
+    public $CTYPEv2 = 'application/x-vnd.kolab.task';
29
+
30
+    public $scheduling_properties = array('start', 'due', 'summary', 'status');
31
+
32
+    protected $objclass = 'Todo';
33
+    protected $read_func = 'readTodo';
34
+    protected $write_func = 'writeTodo';
35
+
36
+
37
+    /**
38
+     * Set properties to the kolabformat object
39
+     *
40
+     * @param array  Object data as hash array
41
+     */
42
+    public function set(&$object)
43
+    {
44
+        // set common xcal properties
45
+        parent::set($object);
46
+
47
+        $this->obj->setPercentComplete(intval($object['complete']));
48
+
49
+        $status = kolabformat::StatusUndefined;
50
+        if ($object['complete'] == 100 && !array_key_exists('status', $object))
51
+            $status = kolabformat::StatusCompleted;
52
+        else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
53
+            $status = $this->status_map[$object['status']];
54
+        $this->obj->setStatus($status);
55
+
56
+        $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly));
57
+        $this->obj->setDue(self::get_datetime($object['due'], null, $object['due']->_dateonly));
58
+
59
+        $related = new vectors;
60
+        if (!empty($object['parent_id']))
61
+            $related->push($object['parent_id']);
62
+        $this->obj->setRelatedTo($related);
63
+
64
+        // cache this data
65
+        $this->data = $object;
66
+        unset($this->data['_formatobj']);
67
+    }
68
+
69
+    /**
70
+     *
71
+     */
72
+    public function is_valid()
73
+    {
74
+        return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
75
+    }
76
+
77
+    /**
78
+     * Convert the Configuration object into a hash array data structure
79
+     *
80
+     * @param array Additional data for merge
81
+     *
82
+     * @return array  Config object data as hash array
83
+     */
84
+    public function to_array($data = array())
85
+    {
86
+        // return cached result
87
+        if (!empty($this->data))
88
+            return $this->data;
89
+
90
+        // read common xcal props
91
+        $object = parent::to_array($data);
92
+
93
+        $object['complete'] = intval($this->obj->percentComplete());
94
+
95
+        // if due date is set
96
+        if ($due = $this->obj->due())
97
+            $object['due'] = self::php_datetime($due);
98
+
99
+        // related-to points to parent task; we only support one relation
100
+        $related = self::vector2array($this->obj->relatedTo());
101
+        if (count($related))
102
+            $object['parent_id'] = $related[0];
103
+
104
+        // TODO: map more properties
105
+
106
+        $this->data = $object;
107
+        return $this->data;
108
+    }
109
+
110
+    /**
111
+     * Callback for kolab_storage_cache to get object specific tags to cache
112
+     *
113
+     * @return array List of tags to save in cache
114
+     */
115
+    public function get_tags()
116
+    {
117
+        $tags = parent::get_tags();
118
+
119
+        if ($this->data['status'] == 'COMPLETED' || ($this->data['complete'] == 100 && empty($this->data['status'])))
120
+            $tags[] = 'x-complete';
121
+
122
+        if ($this->data['priority'] == 1)
123
+            $tags[] = 'x-flagged';
124
+
125
+        if ($this->data['parent_id'])
126
+            $tags[] = 'x-parent:' . $this->data['parent_id'];
127
+
128
+        return $tags;
129
+    }
130
+}
131
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_format_xcal.php Added
633
 
1
@@ -0,0 +1,630 @@
2
+<?php
3
+
4
+/**
5
+ * Xcal based Kolab format class wrapping libkolabxml bindings
6
+ *
7
+ * Base class for xcal-based Kolab groupware objects such as event, todo, journal
8
+ *
9
+ * @version @package_version@
10
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
11
+ *
12
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
13
+ *
14
+ * This program is free software: you can redistribute it and/or modify
15
+ * it under the terms of the GNU Affero General Public License as
16
+ * published by the Free Software Foundation, either version 3 of the
17
+ * License, or (at your option) any later version.
18
+ *
19
+ * This program is distributed in the hope that it will be useful,
20
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
+ * GNU Affero General Public License for more details.
23
+ *
24
+ * You should have received a copy of the GNU Affero General Public License
25
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
26
+ */
27
+
28
+abstract class kolab_format_xcal extends kolab_format
29
+{
30
+    public $CTYPE = 'application/calendar+xml';
31
+
32
+    public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
33
+
34
+    public $scheduling_properties = array('start', 'end', 'location');
35
+
36
+    protected $sensitivity_map = array(
37
+        'public'       => kolabformat::ClassPublic,
38
+        'private'      => kolabformat::ClassPrivate,
39
+        'confidential' => kolabformat::ClassConfidential,
40
+    );
41
+
42
+    protected $role_map = array(
43
+        'REQ-PARTICIPANT' => kolabformat::Required,
44
+        'OPT-PARTICIPANT' => kolabformat::Optional,
45
+        'NON-PARTICIPANT' => kolabformat::NonParticipant,
46
+        'CHAIR' => kolabformat::Chair,
47
+    );
48
+
49
+    protected $cutype_map = array(
50
+        'INDIVIDUAL' => kolabformat::CutypeIndividual,
51
+        'GROUP'      => kolabformat::CutypeGroup,
52
+        'ROOM'       => kolabformat::CutypeRoom,
53
+        'RESOURCE'   => kolabformat::CutypeResource,
54
+        'UNKNOWN'    => kolabformat::CutypeUnknown,
55
+    );
56
+
57
+    protected $rrule_type_map = array(
58
+        'MINUTELY' => RecurrenceRule::Minutely,
59
+        'HOURLY' => RecurrenceRule::Hourly,
60
+        'DAILY' => RecurrenceRule::Daily,
61
+        'WEEKLY' => RecurrenceRule::Weekly,
62
+        'MONTHLY' => RecurrenceRule::Monthly,
63
+        'YEARLY' => RecurrenceRule::Yearly,
64
+    );
65
+
66
+    protected $weekday_map = array(
67
+        'MO' => kolabformat::Monday,
68
+        'TU' => kolabformat::Tuesday,
69
+        'WE' => kolabformat::Wednesday,
70
+        'TH' => kolabformat::Thursday,
71
+        'FR' => kolabformat::Friday,
72
+        'SA' => kolabformat::Saturday,
73
+        'SU' => kolabformat::Sunday,
74
+    );
75
+
76
+    protected $alarm_type_map = array(
77
+        'DISPLAY' => Alarm::DisplayAlarm,
78
+        'EMAIL' => Alarm::EMailAlarm,
79
+        'AUDIO' => Alarm::AudioAlarm,
80
+    );
81
+
82
+    protected $status_map = array(
83
+        'NEEDS-ACTION' => kolabformat::StatusNeedsAction,
84
+        'IN-PROCESS'   => kolabformat::StatusInProcess,
85
+        'COMPLETED'    => kolabformat::StatusCompleted,
86
+        'CANCELLED'    => kolabformat::StatusCancelled,
87
+        'TENTATIVE'    => kolabformat::StatusTentative,
88
+        'CONFIRMED'    => kolabformat::StatusConfirmed,
89
+        'DRAFT'        => kolabformat::StatusDraft,
90
+        'FINAL'        => kolabformat::StatusFinal,
91
+    );
92
+
93
+    protected $part_status_map = array(
94
+        'UNKNOWN' => kolabformat::PartNeedsAction,
95
+        'NEEDS-ACTION' => kolabformat::PartNeedsAction,
96
+        'TENTATIVE' => kolabformat::PartTentative,
97
+        'ACCEPTED' => kolabformat::PartAccepted,
98
+        'DECLINED' => kolabformat::PartDeclined,
99
+        'DELEGATED' => kolabformat::PartDelegated,
100
+      );
101
+
102
+
103
+    /**
104
+     * Convert common xcard properties into a hash array data structure
105
+     *
106
+     * @param array Additional data for merge
107
+     *
108
+     * @return array  Object data as hash array
109
+     */
110
+    public function to_array($data = array())
111
+    {
112
+        // read common object props
113
+        $object = parent::to_array($data);
114
+
115
+        $status_map = array_flip($this->status_map);
116
+        $sensitivity_map = array_flip($this->sensitivity_map);
117
+
118
+        $object += array(
119
+            'sequence'    => intval($this->obj->sequence()),
120
+            'title'       => $this->obj->summary(),
121
+            'location'    => $this->obj->location(),
122
+            'description' => $this->obj->description(),
123
+            'url'         => $this->obj->url(),
124
+            'status'      => $status_map[$this->obj->status()],
125
+            'sensitivity' => $sensitivity_map[$this->obj->classification()],
126
+            'priority'    => $this->obj->priority(),
127
+            'categories'  => self::vector2array($this->obj->categories()),
128
+            'start'       => self::php_datetime($this->obj->start()),
129
+        );
130
+
131
+        if (method_exists($this->obj, 'comment')) {
132
+            $object['comment'] = $this->obj->comment();
133
+        }
134
+
135
+        // read organizer and attendees
136
+        if (($organizer = $this->obj->organizer()) && ($organizer->email() || $organizer->name())) {
137
+            $object['organizer'] = array(
138
+                'email' => $organizer->email(),
139
+                'name' => $organizer->name(),
140
+            );
141
+        }
142
+
143
+        $role_map = array_flip($this->role_map);
144
+        $cutype_map = array_flip($this->cutype_map);
145
+        $part_status_map = array_flip($this->part_status_map);
146
+        $attvec = $this->obj->attendees();
147
+        for ($i=0; $i < $attvec->size(); $i++) {
148
+            $attendee = $attvec->get($i);
149
+            $cr = $attendee->contact();
150
+            if ($cr->email() != $object['organizer']['email']) {
151
+                $delegators = $delegatees = array();
152
+                $vdelegators = $attendee->delegatedFrom();
153
+                for ($j=0; $j < $vdelegators->size(); $j++) {
154
+                    $delegators[] = $vdelegators->get($j)->email();
155
+                }
156
+                $vdelegatees = $attendee->delegatedTo();
157
+                for ($j=0; $j < $vdelegatees->size(); $j++) {
158
+                    $delegatees[] = $vdelegatees->get($j)->email();
159
+                }
160
+
161
+                $object['attendees'][] = array(
162
+                    'role' => $role_map[$attendee->role()],
163
+                    'cutype' => $cutype_map[$attendee->cutype()],
164
+                    'status' => $part_status_map[$attendee->partStat()],
165
+                    'rsvp' => $attendee->rsvp(),
166
+                    'email' => $cr->email(),
167
+                    'name' => $cr->name(),
168
+                    'delegated-from' => $delegators,
169
+                    'delegated-to' => $delegatees,
170
+                );
171
+            }
172
+        }
173
+
174
+        // read recurrence rule
175
+        if (($rr = $this->obj->recurrenceRule()) && $rr->isValid()) {
176
+            $rrule_type_map = array_flip($this->rrule_type_map);
177
+            $object['recurrence'] = array('FREQ' => $rrule_type_map[$rr->frequency()]);
178
+
179
+            if ($intvl = $rr->interval())
180
+                $object['recurrence']['INTERVAL'] = $intvl;
181
+
182
+            if (($count = $rr->count()) && $count > 0) {
183
+                $object['recurrence']['COUNT'] = $count;
184
+            }
185
+            else if ($until = self::php_datetime($rr->end())) {
186
+                $until->setTime($object['start']->format('G'), $object['start']->format('i'), 0);
187
+                $object['recurrence']['UNTIL'] = $until;
188
+            }
189
+
190
+            if (($byday = $rr->byday()) && $byday->size()) {
191
+                $weekday_map = array_flip($this->weekday_map);
192
+                $weekdays = array();
193
+                for ($i=0; $i < $byday->size(); $i++) {
194
+                    $daypos = $byday->get($i);
195
+                    $prefix = $daypos->occurence();
196
+                    $weekdays[] = ($prefix ? $prefix : '') . $weekday_map[$daypos->weekday()];
197
+                }
198
+                $object['recurrence']['BYDAY'] = join(',', $weekdays);
199
+            }
200
+
201
+            if (($bymday = $rr->bymonthday()) && $bymday->size()) {
202
+                $object['recurrence']['BYMONTHDAY'] = join(',', self::vector2array($bymday));
203
+            }
204
+
205
+            if (($bymonth = $rr->bymonth()) && $bymonth->size()) {
206
+                $object['recurrence']['BYMONTH'] = join(',', self::vector2array($bymonth));
207
+            }
208
+
209
+            if ($exdates = $this->obj->exceptionDates()) {
210
+                for ($i=0; $i < $exdates->size(); $i++) {
211
+                    if ($exdate = self::php_datetime($exdates->get($i)))
212
+                        $object['recurrence']['EXDATE'][] = $exdate;
213
+                }
214
+            }
215
+        }
216
+
217
+        if ($rdates = $this->obj->recurrenceDates()) {
218
+            for ($i=0; $i < $rdates->size(); $i++) {
219
+                if ($rdate = self::php_datetime($rdates->get($i)))
220
+                    $object['recurrence']['RDATE'][] = $rdate;
221
+            }
222
+        }
223
+
224
+        // read alarm
225
+        $valarms = $this->obj->alarms();
226
+        $alarm_types = array_flip($this->alarm_type_map);
227
+        $object['valarms'] = array();
228
+        for ($i=0; $i < $valarms->size(); $i++) {
229
+            $alarm = $valarms->get($i);
230
+            $type = $alarm_types[$alarm->type()];
231
+
232
+            if ($type == 'DISPLAY' || $type == 'EMAIL' || $type == 'AUDIO') {  // only some alarms are supported
233
+                $valarm = array(
234
+                    'action' => $type,
235
+                    'summary' => $alarm->summary(),
236
+                    'description' => $alarm->description(),
237
+                );
238
+
239
+                if ($type == 'EMAIL') {
240
+                    $valarm['attendees'] = array();
241
+                    $attvec = $alarm->attendees();
242
+                    for ($j=0; $j < $attvec->size(); $j++) {
243
+                        $cr = $attvec->get($j);
244
+                        $valarm['attendees'][] = $cr->email();
245
+                    }
246
+                }
247
+                else if ($type == 'AUDIO') {
248
+                    $attach = $alarm->audioFile();
249
+                    $valarm['uri'] = $attach->uri();
250
+                }
251
+
252
+                if ($start = self::php_datetime($alarm->start())) {
253
+                    $object['alarms'] = '@' . $start->format('U');
254
+                    $valarm['trigger'] = $start;
255
+                }
256
+                else if ($offset = $alarm->relativeStart()) {
257
+                    $prefix = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
258
+                    $value = $time = '';
259
+                    if      ($w = $offset->weeks())     $value .= $w . 'W';
260
+                    else if ($d = $offset->days())      $value .= $d . 'D';
261
+                    else if ($h = $offset->hours())     $time  .= $h . 'H';
262
+                    else if ($m = $offset->minutes())   $time  .= $m . 'M';
263
+                    else if ($s = $offset->seconds())   $time  .= $s . 'S';
264
+
265
+                    // assume 'at event time'
266
+                    if (empty($value) && empty($time)) {
267
+                        $prefix = '';
268
+                        $time = '0S';
269
+                    }
270
+
271
+                    $object['alarms'] = $prefix . $value . $time;
272
+                    $valarm['trigger'] = $prefix . 'P' . $value . ($time ? 'T' . $time : '');
273
+                }
274
+
275
+                // read alarm duration and repeat properties
276
+                if (($duration = $alarm->duration()) && $duration->isValid()) {
277
+                    $value = $time = '';
278
+                    if      ($w = $duration->weeks())     $value .= $w . 'W';
279
+                    else if ($d = $duration->days())      $value .= $d . 'D';
280
+                    else if ($h = $duration->hours())     $time  .= $h . 'H';
281
+                    else if ($m = $duration->minutes())   $time  .= $m . 'M';
282
+                    else if ($s = $duration->seconds())   $time  .= $s . 'S';
283
+                    $valarm['duration'] = 'P' . $value . ($time ? 'T' . $time : '');
284
+                    $valarm['repeat'] = $alarm->numrepeat();
285
+                }
286
+
287
+                $object['alarms']  .= ':' . $type;  // legacy property
288
+                $object['valarms'][] = array_filter($valarm);
289
+            }
290
+        }
291
+
292
+        $this->get_attachments($object);
293
+
294
+        return $object;
295
+    }
296
+
297
+
298
+    /**
299
+     * Set common xcal properties to the kolabformat object
300
+     *
301
+     * @param array  Event data as hash array
302
+     */
303
+    public function set(&$object)
304
+    {
305
+        $this->init();
306
+
307
+        $is_new = !$this->obj->uid();
308
+        $old_sequence = $this->obj->sequence();
309
+        $reschedule = $is_new;
310
+
311
+        // set common object properties
312
+        parent::set($object);
313
+
314
+        // set sequence value
315
+        if (!isset($object['sequence'])) {
316
+            if ($is_new) {
317
+                $object['sequence'] = 0;
318
+            }
319
+            else {
320
+                $object['sequence'] = $old_sequence;
321
+                $old = $this->data['uid'] ? $this->data : $this->to_array();
322
+
323
+                // increment sequence when updating properties relevant for scheduling.
324
+                // RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
325
+                // TODO: make the list of properties considered 'significant' for scheduling configurable
326
+                foreach ($this->scheduling_properties as $prop) {
327
+                    $a = $old[$prop];
328
+                    $b = $object[$prop];
329
+                    if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
330
+                        $a = $a->format('Y-m-d');
331
+                        $b = $b->format('Y-m-d');
332
+                    }
333
+                    if ($a != $b) {
334
+                        $object['sequence']++;
335
+                        break;
336
+                    }
337
+                }
338
+            }
339
+        }
340
+        $this->obj->setSequence(intval($object['sequence']));
341
+
342
+        if ($object['sequence'] > $old_sequence) {
343
+            $reschedule = true;
344
+        }
345
+
346
+        $this->obj->setSummary($object['title']);
347
+        $this->obj->setLocation($object['location']);
348
+        $this->obj->setDescription($object['description']);
349
+        $this->obj->setPriority($object['priority']);
350
+        $this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
351
+        $this->obj->setCategories(self::array2vector($object['categories']));
352
+        $this->obj->setUrl(strval($object['url']));
353
+
354
+        if (method_exists($this->obj, 'setComment')) {
355
+            $this->obj->setComment($object['comment']);
356
+        }
357
+
358
+        // process event attendees
359
+        $attendees = new vectorattendee;
360
+        foreach ((array)$object['attendees'] as $i => $attendee) {
361
+            if ($attendee['role'] == 'ORGANIZER') {
362
+                $object['organizer'] = $attendee;
363
+            }
364
+            else if ($attendee['email'] != $object['organizer']['email']) {
365
+                $cr = new ContactReference(ContactReference::EmailReference, $attendee['email']);
366
+                $cr->setName($attendee['name']);
367
+
368
+                // set attendee RSVP if missing
369
+                if (!isset($attendee['rsvp'])) {
370
+                    $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = true;
371
+                }
372
+
373
+                $att = new Attendee;
374
+                $att->setContact($cr);
375
+                $att->setPartStat($this->part_status_map[$attendee['status']]);
376
+                $att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
377
+                $att->setCutype($this->cutype_map[$attendee['cutype']] ? $this->cutype_map[$attendee['cutype']] : kolabformat::CutypeIndividual);
378
+                $att->setRSVP((bool)$attendee['rsvp']);
379
+
380
+                if (!empty($attendee['delegated-from'])) {
381
+                    $vdelegators = new vectorcontactref;
382
+                    foreach ((array)$attendee['delegated-from'] as $delegator) {
383
+                        $vdelegators->push(new ContactReference(ContactReference::EmailReference, $delegator));
384
+                    }
385
+                    $att->setDelegatedFrom($vdelegators);
386
+                }
387
+                if (!empty($attendee['delegated-to'])) {
388
+                    $vdelegatees = new vectorcontactref;
389
+                    foreach ((array)$attendee['delegated-to'] as $delegatee) {
390
+                        $vdelegatees->push(new ContactReference(ContactReference::EmailReference, $delegatee));
391
+                    }
392
+                    $att->setDelegatedTo($vdelegatees);
393
+                }
394
+
395
+                if ($att->isValid()) {
396
+                    $attendees->push($att);
397
+                }
398
+                else {
399
+                    rcube::raise_error(array(
400
+                        'code' => 600, 'type' => 'php',
401
+                        'file' => __FILE__, 'line' => __LINE__,
402
+                        'message' => "Invalid event attendee: " . json_encode($attendee),
403
+                    ), true);
404
+                }
405
+            }
406
+        }
407
+        $this->obj->setAttendees($attendees);
408
+
409
+        if ($object['organizer']) {
410
+            $organizer = new ContactReference(ContactReference::EmailReference, $object['organizer']['email']);
411
+            $organizer->setName($object['organizer']['name']);
412
+            $this->obj->setOrganizer($organizer);
413
+        }
414
+
415
+        // save recurrence rule
416
+        $rr = new RecurrenceRule;
417
+        $rr->setFrequency(RecurrenceRule::FreqNone);
418
+
419
+        if ($object['recurrence'] && !empty($object['recurrence']['FREQ'])) {
420
+            $rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
421
+
422
+            if ($object['recurrence']['INTERVAL'])
423
+                $rr->setInterval(intval($object['recurrence']['INTERVAL']));
424
+
425
+            if ($object['recurrence']['BYDAY']) {
426
+                $byday = new vectordaypos;
427
+                foreach (explode(',', $object['recurrence']['BYDAY']) as $day) {
428
+                    $occurrence = 0;
429
+                    if (preg_match('/^([\d-]+)([A-Z]+)$/', $day, $m)) {
430
+                        $occurrence = intval($m[1]);
431
+                        $day = $m[2];
432
+                    }
433
+                    if (isset($this->weekday_map[$day]))
434
+                        $byday->push(new DayPos($occurrence, $this->weekday_map[$day]));
435
+                }
436
+                $rr->setByday($byday);
437
+            }
438
+
439
+            if ($object['recurrence']['BYMONTHDAY']) {
440
+                $bymday = new vectori;
441
+                foreach (explode(',', $object['recurrence']['BYMONTHDAY']) as $day)
442
+                    $bymday->push(intval($day));
443
+                $rr->setBymonthday($bymday);
444
+            }
445
+
446
+            if ($object['recurrence']['BYMONTH']) {
447
+                $bymonth = new vectori;
448
+                foreach (explode(',', $object['recurrence']['BYMONTH']) as $month)
449
+                    $bymonth->push(intval($month));
450
+                $rr->setBymonth($bymonth);
451
+            }
452
+
453
+            if ($object['recurrence']['COUNT'])
454
+                $rr->setCount(intval($object['recurrence']['COUNT']));
455
+            else if ($object['recurrence']['UNTIL'])
456
+                $rr->setEnd(self::get_datetime($object['recurrence']['UNTIL'], null, true));
457
+
458
+            if ($rr->isValid()) {
459
+                // add exception dates (only if recurrence rule is valid)
460
+                $exdates = new vectordatetime;
461
+                foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
462
+                    $exdates->push(self::get_datetime($exdate, null, true));
463
+                $this->obj->setExceptionDates($exdates);
464
+            }
465
+            else {
466
+                rcube::raise_error(array(
467
+                    'code' => 600, 'type' => 'php',
468
+                    'file' => __FILE__, 'line' => __LINE__,
469
+                    'message' => "Invalid event recurrence rule: " . json_encode($object['recurrence']),
470
+                ), true);
471
+            }
472
+        }
473
+
474
+        $this->obj->setRecurrenceRule($rr);
475
+
476
+        // save recurrence dates (aka RDATE)
477
+        if (!empty($object['recurrence']['RDATE'])) {
478
+            $rdates = new vectordatetime;
479
+            foreach ((array)$object['recurrence']['RDATE'] as $rdate)
480
+                $rdates->push(self::get_datetime($rdate, null, true));
481
+            $this->obj->setRecurrenceDates($rdates);
482
+        }
483
+
484
+        // save alarm
485
+        $valarms = new vectoralarm;
486
+        if ($object['valarms']) {
487
+            foreach ($object['valarms'] as $valarm) {
488
+                if (!array_key_exists($valarm['action'], $this->alarm_type_map)) {
489
+                    continue;  // skip unknown alarm types
490
+                }
491
+
492
+                if ($valarm['action'] == 'EMAIL') {
493
+                    $recipients = new vectorcontactref;
494
+                    foreach (($valarm['attendees'] ?: array($object['_owner'])) as $email) {
495
+                        $recipients->push(new ContactReference(ContactReference::EmailReference, $email));
496
+                    }
497
+                    $alarm = new Alarm(
498
+                        strval($valarm['summary'] ?: $object['title']),
499
+                        strval($valarm['description'] ?: $object['description']),
500
+                        $recipients
501
+                    );
502
+                }
503
+                else if ($valarm['action'] == 'AUDIO') {
504
+                    $attach = new Attachment;
505
+                    $attach->setUri($valarm['uri'] ?: 'null', 'unknown');
506
+                    $alarm = new Alarm($attach);
507
+                }
508
+                else {
509
+                    // action == DISPLAY
510
+                    $alarm = new Alarm(strval($valarm['summary'] ?: $object['title']));
511
+                }
512
+
513
+                if (is_object($valarm['trigger']) && $valarm['trigger'] instanceof DateTime) {
514
+                    $alarm->setStart(self::get_datetime($valarm['trigger'], new DateTimeZone('UTC')));
515
+                }
516
+                else {
517
+                    try {
518
+                        $prefix = $valarm['trigger'][0];
519
+                        $period = new DateInterval(preg_replace('/[^0-9PTWDHMS]/', '', $valarm['trigger']));
520
+                        $duration = new Duration($period->d, $period->h, $period->i, $period->s, $prefix == '-');
521
+                    }
522
+                    catch (Exception $e) {
523
+                        // skip alarm with invalid trigger values
524
+                        rcube::raise_error($e, true);
525
+                        continue;
526
+                    }
527
+
528
+                    $alarm->setRelativeStart($duration, $prefix == '-' ? kolabformat::Start : kolabformat::End);
529
+                }
530
+
531
+                if ($valarm['duration']) {
532
+                    try {
533
+                        $d = new DateInterval($valarm['duration']);
534
+                        $duration = new Duration($d->d, $d->h, $d->i, $d->s);
535
+                        $alarm->setDuration($duration, intval($valarm['repeat']));
536
+                    }
537
+                    catch (Exception $e) {
538
+                        // ignore
539
+                    }
540
+                }
541
+
542
+                $valarms->push($alarm);
543
+            }
544
+        }
545
+        // legacy support
546
+        else if ($object['alarms']) {
547
+            list($offset, $type) = explode(":", $object['alarms']);
548
+
549
+            if ($type == 'EMAIL' && !empty($object['_owner'])) {  // email alarms implicitly go to event owner
550
+                $recipients = new vectorcontactref;
551
+                $recipients->push(new ContactReference(ContactReference::EmailReference, $object['_owner']));
552
+                $alarm = new Alarm($object['title'], strval($object['description']), $recipients);
553
+            }
554
+            else {  // default: display alarm
555
+                $alarm = new Alarm($object['title']);
556
+            }
557
+
558
+            if (preg_match('/^@(\d+)/', $offset, $d)) {
559
+                $alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
560
+            }
561
+            else if (preg_match('/^([-+]?)P?T?(\d+)([SMHDW])/', $offset, $d)) {
562
+                $days = $hours = $minutes = $seconds = 0;
563
+                switch ($d[3]) {
564
+                    case 'W': $days  = 7*intval($d[2]); break;
565
+                    case 'D': $days    = intval($d[2]); break;
566
+                    case 'H': $hours   = intval($d[2]); break;
567
+                    case 'M': $minutes = intval($d[2]); break;
568
+                    case 'S': $seconds = intval($d[2]); break;
569
+                }
570
+                $alarm->setRelativeStart(new Duration($days, $hours, $minutes, $seconds, $d[1] == '-'), $d[1] == '-' ? kolabformat::Start : kolabformat::End);
571
+            }
572
+
573
+            $valarms->push($alarm);
574
+        }
575
+        $this->obj->setAlarms($valarms);
576
+
577
+        $this->set_attachments($object);
578
+    }
579
+
580
+    /**
581
+     * Callback for kolab_storage_cache to get words to index for fulltext search
582
+     *
583
+     * @return array List of words to save in cache
584
+     */
585
+    public function get_words()
586
+    {
587
+        $data = '';
588
+        foreach (self::$fulltext_cols as $colname) {
589
+            list($col, $field) = explode(':', $colname);
590
+
591
+            if ($field) {
592
+                $a = array();
593
+                foreach ((array)$this->data[$col] as $attr)
594
+                    $a[] = $attr[$field];
595
+                $val = join(' ', $a);
596
+            }
597
+            else {
598
+                $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
599
+            }
600
+
601
+            if (strlen($val))
602
+                $data .= $val . ' ';
603
+        }
604
+
605
+        return array_unique(rcube_utils::normalize_string($data, true));
606
+    }
607
+
608
+    /**
609
+     * Callback for kolab_storage_cache to get object specific tags to cache
610
+     *
611
+     * @return array List of tags to save in cache
612
+     */
613
+    public function get_tags()
614
+    {
615
+        $tags = array();
616
+
617
+        if (!empty($this->data['valarms'])) {
618
+            $tags[] = 'x-has-alarms';
619
+        }
620
+
621
+        // create tags reflecting participant status
622
+        if (is_array($this->data['attendees'])) {
623
+            foreach ($this->data['attendees'] as $attendee) {
624
+                if (!empty($attendee['email']) && !empty($attendee['status']))
625
+                    $tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
626
+            }
627
+        }
628
+
629
+        return $tags;
630
+    }
631
+}
632
\ No newline at end of file
633
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage.php Added
1573
 
1
@@ -0,0 +1,1571 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab storage class providing static methods to access groupware objects on a Kolab server.
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ * @author Aleksander Machniak <machniak@kolabsys.com>
10
+ *
11
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
12
+ *
13
+ * This program is free software: you can redistribute it and/or modify
14
+ * it under the terms of the GNU Affero General Public License as
15
+ * published by the Free Software Foundation, either version 3 of the
16
+ * License, or (at your option) any later version.
17
+ *
18
+ * This program is distributed in the hope that it will be useful,
19
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
+ * GNU Affero General Public License for more details.
22
+ *
23
+ * You should have received a copy of the GNU Affero General Public License
24
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
25
+ */
26
+
27
+class kolab_storage
28
+{
29
+    const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
30
+    const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
31
+    const COLOR_KEY_SHARED  = '/shared/vendor/kolab/color';
32
+    const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
33
+    const NAME_KEY_SHARED   = '/shared/vendor/kolab/displayname';
34
+    const NAME_KEY_PRIVATE  = '/private/vendor/kolab/displayname';
35
+    const UID_KEY_SHARED    = '/shared/vendor/kolab/uniqueid';
36
+    const UID_KEY_PRIVATE   = '/private/vendor/kolab/uniqueid';
37
+    const UID_KEY_CYRUS     = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
38
+
39
+    const ERROR_IMAP_CONN      = 1;
40
+    const ERROR_CACHE_DB       = 2;
41
+    const ERROR_NO_PERMISSION  = 3;
42
+    const ERROR_INVALID_FOLDER = 4;
43
+
44
+    public static $version = '3.0';
45
+    public static $last_error;
46
+    public static $encode_ids = false;
47
+
48
+    private static $ready = false;
49
+    private static $with_tempsubs = true;
50
+    private static $subscriptions;
51
+    private static $typedata = array();
52
+    private static $states;
53
+    private static $config;
54
+    private static $imap;
55
+    private static $ldap;
56
+
57
+    // Default folder names
58
+    private static $default_folders = array(
59
+        'event'         => 'Calendar',
60
+        'contact'       => 'Contacts',
61
+        'task'          => 'Tasks',
62
+        'note'          => 'Notes',
63
+        'file'          => 'Files',
64
+        'configuration' => 'Configuration',
65
+        'journal'       => 'Journal',
66
+        'mail.inbox'       => 'INBOX',
67
+        'mail.drafts'      => 'Drafts',
68
+        'mail.sentitems'   => 'Sent',
69
+        'mail.wastebasket' => 'Trash',
70
+        'mail.outbox'      => 'Outbox',
71
+        'mail.junkemail'   => 'Junk',
72
+    );
73
+
74
+
75
+    /**
76
+     * Setup the environment needed by the libs
77
+     */
78
+    public static function setup()
79
+    {
80
+        if (self::$ready)
81
+            return true;
82
+
83
+        $rcmail = rcube::get_instance();
84
+        self::$config = $rcmail->config;
85
+        self::$version = strval($rcmail->config->get('kolab_format_version', self::$version));
86
+        self::$imap = $rcmail->get_storage();
87
+        self::$ready = class_exists('kolabformat') &&
88
+            (self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
89
+
90
+        if (self::$ready) {
91
+            // set imap options
92
+            self::$imap->set_options(array(
93
+                'skip_deleted' => true,
94
+                'threading' => false,
95
+            ));
96
+        }
97
+        else if (!class_exists('kolabformat')) {
98
+            rcube::raise_error(array(
99
+                'code' => 900, 'type' => 'php',
100
+                'message' => "required kolabformat module not found"
101
+            ), true);
102
+        }
103
+        else {
104
+            rcube::raise_error(array(
105
+                'code' => 900, 'type' => 'php',
106
+                'message' => "IMAP server doesn't support METADATA or ANNOTATEMORE"
107
+            ), true);
108
+        }
109
+
110
+        return self::$ready;
111
+    }
112
+
113
+    /**
114
+     * Initializes LDAP object to resolve Kolab users
115
+     */
116
+    public static function ldap()
117
+    {
118
+        if (self::$ldap) {
119
+            return self::$ldap;
120
+        }
121
+
122
+        self::setup();
123
+
124
+        $config = self::$config->get('kolab_users_directory', self::$config->get('kolab_auth_addressbook'));
125
+
126
+        if (!is_array($config)) {
127
+            $ldap_config = (array)self::$config->get('ldap_public');
128
+            $config = $ldap_config[$config];
129
+        }
130
+
131
+        if (empty($config)) {
132
+            return null;
133
+        }
134
+
135
+        // overwrite filter option
136
+        if ($filter = self::$config->get('kolab_users_filter')) {
137
+            self::$config->set('kolab_auth_filter', $filter);
138
+        }
139
+
140
+        // re-use the LDAP wrapper class from kolab_auth plugin
141
+        require_once rtrim(RCUBE_PLUGINS_DIR, '/') . '/kolab_auth/kolab_auth_ldap.php';
142
+
143
+        self::$ldap = new kolab_auth_ldap($config);
144
+
145
+        return self::$ldap;
146
+    }
147
+
148
+    /**
149
+     * Get a list of storage folders for the given data type
150
+     *
151
+     * @param string Data type to list folders for (contact,distribution-list,event,task,note)
152
+     * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
153
+     *
154
+     * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
155
+     */
156
+    public static function get_folders($type, $subscribed = null)
157
+    {
158
+        $folders = $folderdata = array();
159
+
160
+        if (self::setup()) {
161
+            foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
162
+                $folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
163
+            }
164
+        }
165
+
166
+        return $folders;
167
+    }
168
+
169
+    /**
170
+     * Getter for the storage folder for the given type
171
+     *
172
+     * @param string Data type to list folders for (contact,distribution-list,event,task,note)
173
+     * @return object kolab_storage_folder  The folder object
174
+     */
175
+    public static function get_default_folder($type)
176
+    {
177
+        if (self::setup()) {
178
+            foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) {
179
+                return new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
180
+            }
181
+        }
182
+
183
+        return null;
184
+    }
185
+
186
+    /**
187
+     * Getter for a specific storage folder
188
+     *
189
+     * @param string IMAP folder to access (UTF7-IMAP)
190
+     * @param string Expected folder type
191
+     *
192
+     * @return object kolab_storage_folder  The folder object
193
+     */
194
+    public static function get_folder($folder, $type = null)
195
+    {
196
+        return self::setup() ? new kolab_storage_folder($folder, $type) : null;
197
+    }
198
+
199
+    /**
200
+     * Getter for a single Kolab object, identified by its UID.
201
+     * This will search all folders storing objects of the given type.
202
+     *
203
+     * @param string Object UID
204
+     * @param string Object type (contact,event,task,journal,file,note,configuration)
205
+     * @return array The Kolab object represented as hash array or false if not found
206
+     */
207
+    public static function get_object($uid, $type)
208
+    {
209
+        self::setup();
210
+        $folder = null;
211
+        foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
212
+            if (!$folder)
213
+                $folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
214
+            else
215
+                $folder->set_folder($foldername, $type, $folderdata[$foldername]);
216
+
217
+            if ($object = $folder->get_object($uid, '*'))
218
+                return $object;
219
+        }
220
+
221
+        return false;
222
+    }
223
+
224
+    /**
225
+     * Execute cross-folder searches with the given query.
226
+     *
227
+     * @param array  Pseudo-SQL query as list of filter parameter triplets
228
+     * @param string Object type (contact,event,task,journal,file,note,configuration)
229
+     * @return array List of Kolab data objects (each represented as hash array)
230
+     * @see kolab_storage_format::select()
231
+     */
232
+    public static function select($query, $type)
233
+    {
234
+        self::setup();
235
+        $folder = null;
236
+        $result = array();
237
+
238
+        foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
239
+            if (!$folder)
240
+                $folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
241
+            else
242
+                $folder->set_folder($foldername, $type, $folderdata[$foldername]);
243
+
244
+            foreach ($folder->select($query, '*') as $object) {
245
+                $result[] = $object;
246
+            }
247
+        }
248
+
249
+        return $result;
250
+    }
251
+
252
+    /**
253
+     * Returns Free-busy server URL
254
+     */
255
+    public static function get_freebusy_server()
256
+    {
257
+        $url = 'https://' . $_SESSION['imap_host'] . '/freebusy';
258
+        $url = self::$config->get('kolab_freebusy_server', $url);
259
+        $url = rcube_utils::resolve_url($url);
260
+
261
+        return unslashify($url);
262
+    }
263
+
264
+    /**
265
+     * Compose an URL to query the free/busy status for the given user
266
+     */
267
+    public static function get_freebusy_url($email)
268
+    {
269
+        return self::get_freebusy_server() . '/' . $email . '.ifb';
270
+    }
271
+
272
+    /**
273
+     * Creates folder ID from folder name
274
+     *
275
+     * @param string  $folder Folder name (UTF7-IMAP)
276
+     * @param boolean $enc    Use lossless encoding
277
+     * @return string Folder ID string
278
+     */
279
+    public static function folder_id($folder, $enc = null)
280
+    {
281
+        return $enc == true || ($enc === null && self::$encode_ids) ?
282
+            self::id_encode($folder) :
283
+            asciiwords(strtr($folder, '/.-', '___'));
284
+    }
285
+
286
+    /**
287
+     * Encode the given ID to a safe ascii representation
288
+     *
289
+     * @param string $id Arbitrary identifier string
290
+     *
291
+     * @return string Ascii representation
292
+     */
293
+    public static function id_encode($id)
294
+    {
295
+        return rtrim(strtr(base64_encode($id), '+/', '-_'), '=');
296
+    }
297
+
298
+    /**
299
+     * Convert the given identifier back to it's raw value
300
+     *
301
+     * @param string $id Ascii identifier
302
+     * @return string Raw identifier string
303
+     */
304
+    public static function id_decode($id)
305
+    {
306
+      return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
307
+    }
308
+
309
+    /**
310
+     * Return the (first) path of the requested IMAP namespace
311
+     *
312
+     * @param string  Namespace name (personal, shared, other)
313
+     * @return string IMAP root path for that namespace
314
+     */
315
+    public static function namespace_root($name)
316
+    {
317
+        foreach ((array)self::$imap->get_namespace($name) as $paths) {
318
+            if (strlen($paths[0]) > 1) {
319
+                return $paths[0];
320
+            }
321
+        }
322
+
323
+        return '';
324
+    }
325
+
326
+
327
+    /**
328
+     * Deletes IMAP folder
329
+     *
330
+     * @param string $name Folder name (UTF7-IMAP)
331
+     *
332
+     * @return bool True on success, false on failure
333
+     */
334
+    public static function folder_delete($name)
335
+    {
336
+        // clear cached entries first
337
+        if ($folder = self::get_folder($name))
338
+            $folder->cache->purge();
339
+
340
+        $rcmail = rcube::get_instance();
341
+        $plugin = $rcmail->plugins->exec_hook('folder_delete', array('name' => $name));
342
+
343
+        $success = self::$imap->delete_folder($name);
344
+        self::$last_error = self::$imap->get_error_str();
345
+
346
+        return $success;
347
+    }
348
+
349
+    /**
350
+     * Creates IMAP folder
351
+     *
352
+     * @param string $name       Folder name (UTF7-IMAP)
353
+     * @param string $type       Folder type
354
+     * @param bool   $subscribed Sets folder subscription
355
+     * @param bool   $active     Sets folder state (client-side subscription)
356
+     *
357
+     * @return bool True on success, false on failure
358
+     */
359
+    public static function folder_create($name, $type = null, $subscribed = false, $active = false)
360
+    {
361
+        self::setup();
362
+
363
+        $rcmail = rcube::get_instance();
364
+        $plugin = $rcmail->plugins->exec_hook('folder_create', array('record' => array(
365
+            'name' => $name,
366
+            'subscribe' => $subscribed,
367
+        )));
368
+
369
+        if ($saved = self::$imap->create_folder($name, $subscribed)) {
370
+            // set metadata for folder type
371
+            if ($type) {
372
+                $saved = self::set_folder_type($name, $type);
373
+
374
+                // revert if metadata could not be set
375
+                if (!$saved) {
376
+                    self::$imap->delete_folder($name);
377
+                }
378
+                // activate folder
379
+                else if ($active) {
380
+                    self::set_state($name, true);
381
+                }
382
+            }
383
+        }
384
+
385
+        if ($saved) {
386
+            return true;
387
+        }
388
+
389
+        self::$last_error = self::$imap->get_error_str();
390
+        return false;
391
+    }
392
+
393
+
394
+    /**
395
+     * Renames IMAP folder
396
+     *
397
+     * @param string $oldname Old folder name (UTF7-IMAP)
398
+     * @param string $newname New folder name (UTF7-IMAP)
399
+     *
400
+     * @return bool True on success, false on failure
401
+     */
402
+    public static function folder_rename($oldname, $newname)
403
+    {
404
+        self::setup();
405
+
406
+        $rcmail = rcube::get_instance();
407
+        $plugin = $rcmail->plugins->exec_hook('folder_rename', array(
408
+            'oldname' => $oldname, 'newname' => $newname));
409
+
410
+        $oldfolder = self::get_folder($oldname);
411
+        $active    = self::folder_is_active($oldname);
412
+        $success   = self::$imap->rename_folder($oldname, $newname);
413
+        self::$last_error = self::$imap->get_error_str();
414
+
415
+        // pass active state to new folder name
416
+        if ($success && $active) {
417
+            self::set_state($oldname, false);
418
+            self::set_state($newname, true);
419
+        }
420
+
421
+        // assign existing cache entries to new resource uri
422
+        if ($success && $oldfolder) {
423
+            $oldfolder->cache->rename($newname);
424
+        }
425
+
426
+        return $success;
427
+    }
428
+
429
+
430
+    /**
431
+     * Rename or Create a new IMAP folder.
432
+     *
433
+     * Does additional checks for permissions and folder name restrictions
434
+     *
435
+     * @param array Hash array with folder properties and metadata
436
+     *  - name:       Folder name
437
+     *  - oldname:    Old folder name when changed
438
+     *  - parent:     Parent folder to create the new one in
439
+     *  - type:       Folder type to create
440
+     *  - subscribed: Subscribed flag (IMAP subscription)
441
+     *  - active:     Activation flag (client-side subscription)
442
+     * @return mixed New folder name or False on failure
443
+     */
444
+    public static function folder_update(&$prop)
445
+    {
446
+        self::setup();
447
+
448
+        $folder    = rcube_charset::convert($prop['name'], RCUBE_CHARSET, 'UTF7-IMAP');
449
+        $oldfolder = $prop['oldname']; // UTF7
450
+        $parent    = $prop['parent']; // UTF7
451
+        $delimiter = self::$imap->get_hierarchy_delimiter();
452
+
453
+        if (strlen($oldfolder)) {
454
+            $options = self::$imap->folder_info($oldfolder);
455
+        }
456
+
457
+        if (!empty($options) && ($options['norename'] || $options['protected'])) {
458
+        }
459
+        // sanity checks (from steps/settings/save_folder.inc)
460
+        else if (!strlen($folder)) {
461
+            self::$last_error = 'cannotbeempty';
462
+            return false;
463
+        }
464
+        else if (strlen($folder) > 128) {
465
+            self::$last_error = 'nametoolong';
466
+            return false;
467
+        }
468
+        else {
469
+            // these characters are problematic e.g. when used in LIST/LSUB
470
+            foreach (array($delimiter, '%', '*') as $char) {
471
+                if (strpos($folder, $char) !== false) {
472
+                    self::$last_error = 'forbiddencharacter';
473
+                    return false;
474
+                }
475
+            }
476
+        }
477
+
478
+        if (!empty($options) && ($options['protected'] || $options['norename'])) {
479
+            $folder = $oldfolder;
480
+        }
481
+        else if (strlen($parent)) {
482
+            $folder = $parent . $delimiter . $folder;
483
+        }
484
+        else {
485
+            // add namespace prefix (when needed)
486
+            $folder = self::$imap->mod_folder($folder, 'in');
487
+        }
488
+
489
+        // Check access rights to the parent folder
490
+        if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
491
+            $parent_opts = self::$imap->folder_info($parent);
492
+            if ($parent_opts['namespace'] != 'personal'
493
+                && (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
494
+            ) {
495
+                self::$last_error = 'No permission to create folder';
496
+                return false;
497
+          }
498
+        }
499
+
500
+        // update the folder name
501
+        if (strlen($oldfolder)) {
502
+            if ($oldfolder != $folder) {
503
+                $result = self::folder_rename($oldfolder, $folder);
504
+          }
505
+          else
506
+              $result = true;
507
+        }
508
+        // create new folder
509
+        else {
510
+            $result = self::folder_create($folder, $prop['type'], $prop['subscribed'], $prop['active']);
511
+        }
512
+
513
+        if ($result) {
514
+            self::set_folder_props($folder, $prop);
515
+        }
516
+
517
+        return $result ? $folder : false;
518
+    }
519
+
520
+
521
+    /**
522
+     * Getter for human-readable name of Kolab object (folder)
523
+     * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
524
+     *
525
+     * @param string $folder    IMAP folder name (UTF7-IMAP)
526
+     * @param string $folder_ns Will be set to namespace name of the folder
527
+     *
528
+     * @return string Name of the folder-object
529
+     */
530
+    public static function object_name($folder, &$folder_ns=null)
531
+    {
532
+        self::setup();
533
+
534
+        // find custom display name in folder METADATA
535
+        if ($name = self::custom_displayname($folder)) {
536
+            return $name;
537
+        }
538
+
539
+        $found     = false;
540
+        $namespace = self::$imap->get_namespace();
541
+
542
+        if (!empty($namespace['shared'])) {
543
+            foreach ($namespace['shared'] as $ns) {
544
+                if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
545
+                    $prefix = '';
546
+                    $folder = substr($folder, strlen($ns[0]));
547
+                    $delim  = $ns[1];
548
+                    $found  = true;
549
+                    $folder_ns = 'shared';
550
+                    break;
551
+                }
552
+            }
553
+        }
554
+        if (!$found && !empty($namespace['other'])) {
555
+            foreach ($namespace['other'] as $ns) {
556
+                if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
557
+                    // remove namespace prefix
558
+                    $folder = substr($folder, strlen($ns[0]));
559
+                    $delim  = $ns[1];
560
+                    // get username
561
+                    $pos    = strpos($folder, $delim);
562
+                    if ($pos) {
563
+                        $prefix = '('.substr($folder, 0, $pos).')';
564
+                        $folder = substr($folder, $pos+1);
565
+                    }
566
+                    else {
567
+                        $prefix = '('.$folder.')';
568
+                        $folder = '';
569
+                    }
570
+
571
+                    $found  = true;
572
+                    $folder_ns = 'other';
573
+                    break;
574
+                }
575
+            }
576
+        }
577
+        if (!$found && !empty($namespace['personal'])) {
578
+            foreach ($namespace['personal'] as $ns) {
579
+                if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
580
+                    // remove namespace prefix
581
+                    $folder = substr($folder, strlen($ns[0]));
582
+                    $prefix = '';
583
+                    $delim  = $ns[1];
584
+                    $found  = true;
585
+                    break;
586
+                }
587
+            }
588
+        }
589
+
590
+        if (empty($delim))
591
+            $delim = self::$imap->get_hierarchy_delimiter();
592
+
593
+        $folder = rcube_charset::convert($folder, 'UTF7-IMAP');
594
+        $folder = html::quote($folder);
595
+        $folder = str_replace(html::quote($delim), ' &raquo; ', $folder);
596
+
597
+        if ($prefix)
598
+            $folder = html::quote($prefix) . ($folder !== '' ? ' ' . $folder : '');
599
+
600
+        if (!$folder_ns)
601
+            $folder_ns = 'personal';
602
+
603
+        return $folder;
604
+    }
605
+
606
+    /**
607
+     * Get custom display name (saved in metadata) for the given folder
608
+     */
609
+    public static function custom_displayname($folder)
610
+    {
611
+      // find custom display name in folder METADATA
612
+      if (self::$config->get('kolab_custom_display_names', true)) {
613
+          $metadata = self::$imap->get_metadata($folder, array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
614
+          if (($name = $metadata[$folder][self::NAME_KEY_PRIVATE]) || ($name = $metadata[$folder][self::NAME_KEY_SHARED])) {
615
+              return $name;
616
+          }
617
+      }
618
+
619
+      return false;
620
+    }
621
+
622
+    /**
623
+     * Helper method to generate a truncated folder name to display.
624
+     * Note: $origname is a string returned by self::object_name()
625
+     */
626
+    public static function folder_displayname($origname, &$names)
627
+    {
628
+        $name = $origname;
629
+
630
+        // find folder prefix to truncate
631
+        for ($i = count($names)-1; $i >= 0; $i--) {
632
+            if (strpos($name, $names[$i] . ' &raquo; ') === 0) {
633
+                $length = strlen($names[$i] . ' &raquo; ');
634
+                $prefix = substr($name, 0, $length);
635
+                $count  = count(explode(' &raquo; ', $prefix));
636
+                $diff   = 1;
637
+
638
+                // check if prefix folder is in other users namespace
639
+                for ($n = count($names)-1; $n >= 0; $n--) {
640
+                    if (strpos($prefix, '(' . $names[$n] . ') ') === 0) {
641
+                        $diff = 0;
642
+                        break;
643
+                    }
644
+                }
645
+
646
+                $name = str_repeat('&nbsp;&nbsp;&nbsp;', $count - $diff) . '&raquo; ' . substr($name, $length);
647
+                break;
648
+            }
649
+            // other users namespace and parent folder exists
650
+            else if (strpos($name, '(' . $names[$i] . ') ') === 0) {
651
+                $length = strlen('(' . $names[$i] . ') ');
652
+                $prefix = substr($name, 0, $length);
653
+                $count  = count(explode(' &raquo; ', $prefix));
654
+                $name   = str_repeat('&nbsp;&nbsp;&nbsp;', $count) . '&raquo; ' . substr($name, $length);
655
+                break;
656
+            }
657
+        }
658
+
659
+        $names[] = $origname;
660
+
661
+        return $name;
662
+    }
663
+
664
+
665
+    /**
666
+     * Creates a SELECT field with folders list
667
+     *
668
+     * @param string $type    Folder type
669
+     * @param array  $attrs   SELECT field attributes (e.g. name)
670
+     * @param string $current The name of current folder (to skip it)
671
+     *
672
+     * @return html_select SELECT object
673
+     */
674
+    public static function folder_selector($type, $attrs, $current = '')
675
+    {
676
+        // get all folders of specified type (sorted)
677
+        $folders = self::get_folders($type, true);
678
+
679
+        $delim = self::$imap->get_hierarchy_delimiter();
680
+        $names = array();
681
+        $len   = strlen($current);
682
+
683
+        if ($len && ($rpos = strrpos($current, $delim))) {
684
+            $parent = substr($current, 0, $rpos);
685
+            $p_len  = strlen($parent);
686
+        }
687
+
688
+        // Filter folders list
689
+        foreach ($folders as $c_folder) {
690
+            $name = $c_folder->name;
691
+
692
+            // skip current folder and it's subfolders
693
+            if ($len) {
694
+                if ($name == $current) {
695
+                    // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
696
+                    if ($p_len && !isset($names[$parent])) {
697
+                        $names[$parent] = self::object_name($parent);
698
+                    }
699
+                    continue;
700
+                }
701
+                if (strpos($name, $current.$delim) === 0) {
702
+                    continue;
703
+                }
704
+            }
705
+
706
+            // always show the parent of current folder
707
+            if ($p_len && $name == $parent) {
708
+            }
709
+            // skip folders where user have no rights to create subfolders
710
+            else if ($c_folder->get_owner() != $_SESSION['username']) {
711
+                $rights = $c_folder->get_myrights();
712
+                if (!preg_match('/[ck]/', $rights)) {
713
+                    continue;
714
+                }
715
+            }
716
+
717
+            $names[$name] = self::object_name($name);
718
+        }
719
+
720
+        // Build SELECT field of parent folder
721
+        $attrs['is_escaped'] = true;
722
+        $select = new html_select($attrs);
723
+        $select->add('---', '');
724
+
725
+        $listnames = array();
726
+        foreach (array_keys($names) as $imap_name) {
727
+            $name = $origname = $names[$imap_name];
728
+
729
+            // find folder prefix to truncate
730
+            for ($i = count($listnames)-1; $i >= 0; $i--) {
731
+                if (strpos($name, $listnames[$i].' &raquo; ') === 0) {
732
+                    $length = strlen($listnames[$i].' &raquo; ');
733
+                    $prefix = substr($name, 0, $length);
734
+                    $count  = count(explode(' &raquo; ', $prefix));
735
+                    $name   = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
736
+                    break;
737
+                }
738
+            }
739
+
740
+            $listnames[] = $origname;
741
+            $select->add($name, $imap_name);
742
+        }
743
+
744
+        return $select;
745
+    }
746
+
747
+
748
+    /**
749
+     * Returns a list of folder names
750
+     *
751
+     * @param string  Optional root folder
752
+     * @param string  Optional name pattern
753
+     * @param string  Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
754
+     * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
755
+     * @param array   Will be filled with folder-types data
756
+     *
757
+     * @return array List of folders
758
+     */
759
+    public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
760
+    {
761
+        if (!self::setup()) {
762
+            return null;
763
+        }
764
+
765
+        // use IMAP subscriptions
766
+        if ($subscribed === null && self::$config->get('kolab_use_subscriptions')) {
767
+            $subscribed = true;
768
+        }
769
+
770
+        if (!$filter) {
771
+            // Get ALL folders list, standard way
772
+            if ($subscribed) {
773
+                $folders = self::$imap->list_folders_subscribed($root, $mbox);
774
+                // add temporarily subscribed folders
775
+                if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
776
+                    $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
777
+                }
778
+            }
779
+            else {
780
+                $folders = self::_imap_list_folders($root, $mbox);
781
+            }
782
+
783
+            return $folders;
784
+        }
785
+        $prefix = $root . $mbox;
786
+        $regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
787
+
788
+        // get folders types for all folders
789
+        if (!$subscribed || $prefix == '*' || !self::$config->get('kolab_skip_namespace')) {
790
+            $folderdata = self::folders_typedata($prefix);
791
+        }
792
+        else {
793
+            // fetch folder types for the effective list of (subscribed) folders when post-filtering
794
+            $folderdata = array();
795
+        }
796
+
797
+        if (!is_array($folderdata)) {
798
+            return array();
799
+        }
800
+
801
+        // In some conditions we can skip LIST command (?)
802
+        if (!$subscribed && $filter != 'mail' && $prefix == '*') {
803
+            foreach ($folderdata as $folder => $type) {
804
+                if (!preg_match($regexp, $type)) {
805
+                    unset($folderdata[$folder]);
806
+                }
807
+            }
808
+
809
+            return self::$imap->sort_folder_list(array_keys($folderdata), true);
810
+        }
811
+
812
+        // Get folders list
813
+        if ($subscribed) {
814
+            $folders = self::$imap->list_folders_subscribed($root, $mbox);
815
+
816
+            // add temporarily subscribed folders
817
+            if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
818
+                $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
819
+            }
820
+        }
821
+        else {
822
+            $folders = self::_imap_list_folders($root, $mbox);
823
+        }
824
+
825
+        // In case of an error, return empty list (?)
826
+        if (!is_array($folders)) {
827
+            return array();
828
+        }
829
+
830
+        // Filter folders list
831
+        foreach ($folders as $idx => $folder) {
832
+            // lookup folder type
833
+            if (!array_key_exists($folder, $folderdata)) {
834
+                $folderdata[$folder] = self::folder_type($folder);
835
+            }
836
+
837
+            $type = $folderdata[$folder];
838
+
839
+            if ($filter == 'mail' && empty($type)) {
840
+                continue;
841
+            }
842
+            if (empty($type) || !preg_match($regexp, $type)) {
843
+                unset($folders[$idx]);
844
+            }
845
+        }
846
+
847
+        return $folders;
848
+    }
849
+
850
+    /**
851
+     * Wrapper for rcube_imap::list_folders() with optional post-filtering
852
+     */
853
+    protected static function _imap_list_folders($root, $mbox)
854
+    {
855
+        $postfilter = null;
856
+
857
+        // compose a post-filter expression for the excluded namespaces
858
+        if ($root . $mbox == '*' && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
859
+            $excludes = array();
860
+            foreach ((array)$skip_ns as $ns) {
861
+                if ($ns_root = self::namespace_root($ns)) {
862
+                    $excludes[] = $ns_root;
863
+                }
864
+            }
865
+
866
+            if (count($excludes)) {
867
+                $postfilter = '!^(' . join(')|(', array_map('preg_quote', $excludes)) . ')!';
868
+            }
869
+        }
870
+
871
+        // use normal LIST command to return all folders, it's fast enough
872
+        $folders = self::$imap->list_folders($root, $mbox, null, null, !empty($postfilter));
873
+
874
+        if (!empty($postfilter)) {
875
+            $folders = array_filter($folders, function($folder) use ($postfilter) { return !preg_match($postfilter, $folder); });
876
+            $folders = self::$imap->sort_folder_list($folders);
877
+        }
878
+
879
+        return $folders;
880
+    }
881
+
882
+
883
+    /**
884
+     * Search for shared or otherwise not listed groupware folders the user has access
885
+     *
886
+     * @param string Folder type of folders to search for
887
+     * @param string Search string
888
+     * @param array  Namespace(s) to exclude results from
889
+     *
890
+     * @return array List of matching kolab_storage_folder objects
891
+     */
892
+    public static function search_folders($type, $query, $exclude_ns = array())
893
+    {
894
+        if (!self::setup()) {
895
+            return array();
896
+        }
897
+
898
+        $folders = array();
899
+        $query = str_replace('*', '', $query);
900
+
901
+        // find unsubscribed IMAP folders of the given type
902
+        foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
903
+            // FIXME: only consider the last part of the folder path for searching?
904
+            $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
905
+            if (($query == '' || strpos($realname, $query) !== false) &&
906
+                !self::folder_is_subscribed($foldername, true) &&
907
+                !in_array(self::$imap->folder_namespace($foldername), (array)$exclude_ns)
908
+              ) {
909
+                $folders[] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
910
+            }
911
+        }
912
+
913
+        return $folders;
914
+    }
915
+
916
+
917
+    /**
918
+     * Sort the given list of kolab folders by namespace/name
919
+     *
920
+     * @param array List of kolab_storage_folder objects
921
+     * @return array Sorted list of folders
922
+     */
923
+    public static function sort_folders($folders)
924
+    {
925
+        $pad     = '  ';
926
+        $out     = array();
927
+        $nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
928
+
929
+        foreach ($folders as $folder) {
930
+            $folders[$folder->name] = $folder;
931
+            $ns = $folder->get_namespace();
932
+            $nsnames[$ns][$folder->name] = strtolower(html_entity_decode(self::object_name($folder->name, $ns), ENT_COMPAT, RCUBE_CHARSET)) . $pad;  // decode &raquo;
933
+        }
934
+
935
+        // $folders is a result of get_folders() we can assume folders were already sorted
936
+        foreach (array_keys($nsnames) as $ns) {
937
+            asort($nsnames[$ns], SORT_LOCALE_STRING);
938
+            foreach (array_keys($nsnames[$ns]) as $utf7name) {
939
+                $out[] = $folders[$utf7name];
940
+            }
941
+        }
942
+
943
+        return $out;
944
+    }
945
+
946
+
947
+    /**
948
+     * Check the folder tree and add the missing parents as virtual folders
949
+     *
950
+     * @param array $folders Folders list
951
+     * @param object $tree   Reference to the root node of the folder tree
952
+     *
953
+     * @return array Flat folders list
954
+     */
955
+    public static function folder_hierarchy($folders, &$tree = null)
956
+    {
957
+        $_folders = array();
958
+        $delim    = self::$imap->get_hierarchy_delimiter();
959
+        $other_ns = rtrim(self::namespace_root('other'), $delim);
960
+        $tree     = new kolab_storage_folder_virtual('', '<root>', '');  // create tree root
961
+        $refs     = array('' => $tree);
962
+
963
+        foreach ($folders as $idx => $folder) {
964
+            $path = explode($delim, $folder->name);
965
+            array_pop($path);
966
+            $folder->parent = join($delim, $path);
967
+            $folder->children = array();  // reset list
968
+
969
+            // skip top folders or ones with a custom displayname
970
+            if (count($path) < 1 || kolab_storage::custom_displayname($folder->name)) {
971
+                $tree->children[] = $folder;
972
+            }
973
+            else {
974
+                $parents = array();
975
+                $depth = $folder->get_namespace() == 'personal' ? 1 : 2;
976
+
977
+                while (count($path) >= $depth && ($parent = join($delim, $path))) {
978
+                    array_pop($path);
979
+                    $parent_parent = join($delim, $path);
980
+                    if (!$refs[$parent]) {
981
+                        if ($folder->type && self::folder_type($parent) == $folder->type) {
982
+                            $refs[$parent] = new kolab_storage_folder($parent, $folder->type, $folder->type);
983
+                            $refs[$parent]->parent = $parent_parent;
984
+                        }
985
+                        else if ($parent_parent == $other_ns) {
986
+                            $refs[$parent] = new kolab_storage_folder_user($parent, $parent_parent);
987
+                        }
988
+                        else {
989
+                            $name = kolab_storage::object_name($parent, $folder->get_namespace());
990
+                            $refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
991
+                        }
992
+                        $parents[] = $refs[$parent];
993
+                    }
994
+                }
995
+
996
+                if (!empty($parents)) {
997
+                    $parents = array_reverse($parents);
998
+                    foreach ($parents as $parent) {
999
+                        $parent_node = $refs[$parent->parent] ?: $tree;
1000
+                        $parent_node->children[] = $parent;
1001
+                        $_folders[] = $parent;
1002
+                    }
1003
+                }
1004
+
1005
+                $parent_node = $refs[$folder->parent] ?: $tree;
1006
+                $parent_node->children[] = $folder;
1007
+            }
1008
+
1009
+            $refs[$folder->name] = $folder;
1010
+            $_folders[] = $folder;
1011
+            unset($folders[$idx]);
1012
+        }
1013
+
1014
+        return $_folders;
1015
+    }
1016
+
1017
+
1018
+    /**
1019
+     * Returns folder types indexed by folder name
1020
+     *
1021
+     * @param string $prefix Folder prefix (Default '*' for all folders)
1022
+     *
1023
+     * @return array|bool List of folders, False on failure
1024
+     */
1025
+    public static function folders_typedata($prefix = '*')
1026
+    {
1027
+        if (!self::setup()) {
1028
+            return false;
1029
+        }
1030
+
1031
+        // return cached result
1032
+        if (is_array(self::$typedata[$prefix])) {
1033
+            return self::$typedata[$prefix];
1034
+        }
1035
+
1036
+        $type_keys = array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE);
1037
+
1038
+        // fetch metadata from *some* folders only
1039
+        if (($prefix == '*' || $prefix == '') && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
1040
+            $delimiter = self::$imap->get_hierarchy_delimiter();
1041
+            $folderdata = $blacklist = array();
1042
+            foreach ((array)$skip_ns as $ns) {
1043
+                if ($ns_root = rtrim(self::namespace_root($ns), $delimiter)) {
1044
+                    $blacklist[] = $ns_root;
1045
+                }
1046
+            }
1047
+            foreach (array('personal','other','shared') as $ns) {
1048
+                if (!in_array($ns, (array)$skip_ns)) {
1049
+                    $ns_root = rtrim(self::namespace_root($ns), $delimiter);
1050
+
1051
+                    // list top-level folders and their childs one by one
1052
+                    // GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
1053
+                    if ($ns_root == '') {
1054
+                        foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
1055
+                            if (!in_array($folder, $blacklist)) {
1056
+                                $folderdata[$folder] = $metadata;
1057
+                                $opts = self::$imap->folder_attributes($folder);
1058
+                                if (!in_array('\\HasNoChildren', $opts) && ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) {
1059
+                                    $folderdata += $data;
1060
+                                }
1061
+                            }
1062
+                        }
1063
+                    }
1064
+                    else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) {
1065
+                        $folderdata += $data;
1066
+                    }
1067
+                }
1068
+            }
1069
+        }
1070
+        else {
1071
+            $folderdata = self::$imap->get_metadata($prefix, $type_keys);
1072
+        }
1073
+
1074
+        if (!is_array($folderdata)) {
1075
+            return false;
1076
+        }
1077
+
1078
+        // keep list in memory
1079
+        self::$typedata[$prefix] = array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
1080
+
1081
+        return self::$typedata[$prefix];
1082
+    }
1083
+
1084
+
1085
+    /**
1086
+     * Callback for array_map to select the correct annotation value
1087
+     */
1088
+    public static function folder_select_metadata($types)
1089
+    {
1090
+        if (!empty($types[self::CTYPE_KEY_PRIVATE])) {
1091
+            return $types[self::CTYPE_KEY_PRIVATE];
1092
+        }
1093
+        else if (!empty($types[self::CTYPE_KEY])) {
1094
+            list($ctype, ) = explode('.', $types[self::CTYPE_KEY]);
1095
+            return $ctype;
1096
+        }
1097
+        return null;
1098
+    }
1099
+
1100
+
1101
+    /**
1102
+     * Returns type of IMAP folder
1103
+     *
1104
+     * @param string $folder Folder name (UTF7-IMAP)
1105
+     *
1106
+     * @return string Folder type
1107
+     */
1108
+    public static function folder_type($folder)
1109
+    {
1110
+        self::setup();
1111
+
1112
+        // return in-memory cached result
1113
+        foreach (self::$typedata as $typedata) {
1114
+            if (array_key_exists($folder, $typedata)) {
1115
+                return $typedata[$folder];
1116
+            }
1117
+        }
1118
+
1119
+        $metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
1120
+
1121
+        if (!is_array($metadata)) {
1122
+            return null;
1123
+        }
1124
+
1125
+        if (!empty($metadata[$folder])) {
1126
+            return self::folder_select_metadata($metadata[$folder]);
1127
+        }
1128
+
1129
+        return 'mail';
1130
+    }
1131
+
1132
+
1133
+    /**
1134
+     * Sets folder content-type.
1135
+     *
1136
+     * @param string $folder Folder name
1137
+     * @param string $type   Content type
1138
+     *
1139
+     * @return boolean True on success
1140
+     */
1141
+    public static function set_folder_type($folder, $type='mail')
1142
+    {
1143
+        self::setup();
1144
+
1145
+        list($ctype, $subtype) = explode('.', $type);
1146
+
1147
+        $success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null));
1148
+
1149
+        if (!$success)  // fallback: only set private annotation
1150
+            $success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type));
1151
+
1152
+        return $success;
1153
+    }
1154
+
1155
+
1156
+    /**
1157
+     * Check subscription status of this folder
1158
+     *
1159
+     * @param string $folder Folder name
1160
+     * @param boolean $temp  Include temporary/session subscriptions
1161
+     *
1162
+     * @return boolean True if subscribed, false if not
1163
+     */
1164
+    public static function folder_is_subscribed($folder, $temp = false)
1165
+    {
1166
+        if (self::$subscriptions === null) {
1167
+            self::setup();
1168
+            self::$with_tempsubs = false;
1169
+            self::$subscriptions = self::$imap->list_folders_subscribed();
1170
+            self::$with_tempsubs = true;
1171
+        }
1172
+
1173
+        return in_array($folder, self::$subscriptions) ||
1174
+            ($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
1175
+    }
1176
+
1177
+
1178
+    /**
1179
+     * Change subscription status of this folder
1180
+     *
1181
+     * @param string $folder Folder name
1182
+     * @param boolean $temp  Only subscribe temporarily for the current session
1183
+     *
1184
+     * @return True on success, false on error
1185
+     */
1186
+    public static function folder_subscribe($folder, $temp = false)
1187
+    {
1188
+        self::setup();
1189
+
1190
+        // temporary/session subscription
1191
+        if ($temp) {
1192
+            if (self::folder_is_subscribed($folder)) {
1193
+                return true;
1194
+            }
1195
+            else if (!is_array($_SESSION['kolab_subscribed_folders']) || !in_array($folder, $_SESSION['kolab_subscribed_folders'])) {
1196
+                $_SESSION['kolab_subscribed_folders'][] = $folder;
1197
+                return true;
1198
+            }
1199
+        }
1200
+        else if (self::$imap->subscribe($folder)) {
1201
+            self::$subscriptions = null;
1202
+            return true;
1203
+        }
1204
+
1205
+        return false;
1206
+    }
1207
+
1208
+
1209
+    /**
1210
+     * Change subscription status of this folder
1211
+     *
1212
+     * @param string $folder Folder name
1213
+     * @param boolean $temp  Only remove temporary subscription
1214
+     *
1215
+     * @return True on success, false on error
1216
+     */
1217
+    public static function folder_unsubscribe($folder, $temp = false)
1218
+    {
1219
+        self::setup();
1220
+
1221
+        // temporary/session subscription
1222
+        if ($temp) {
1223
+            if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
1224
+                unset($_SESSION['kolab_subscribed_folders'][$i]);
1225
+            }
1226
+            return true;
1227
+        }
1228
+        else if (self::$imap->unsubscribe($folder)) {
1229
+            self::$subscriptions = null;
1230
+            return true;
1231
+        }
1232
+
1233
+        return false;
1234
+    }
1235
+
1236
+
1237
+    /**
1238
+     * Check activation status of this folder
1239
+     *
1240
+     * @param string $folder Folder name
1241
+     *
1242
+     * @return boolean True if active, false if not
1243
+     */
1244
+    public static function folder_is_active($folder)
1245
+    {
1246
+        $active_folders = self::get_states();
1247
+
1248
+        return in_array($folder, $active_folders);
1249
+    }
1250
+
1251
+
1252
+    /**
1253
+     * Change activation status of this folder
1254
+     *
1255
+     * @param string $folder Folder name
1256
+     *
1257
+     * @return True on success, false on error
1258
+     */
1259
+    public static function folder_activate($folder)
1260
+    {
1261
+        // activation implies temporary subscription
1262
+        self::folder_subscribe($folder, true);
1263
+        return self::set_state($folder, true);
1264
+    }
1265
+
1266
+
1267
+    /**
1268
+     * Change activation status of this folder
1269
+     *
1270
+     * @param string $folder Folder name
1271
+     *
1272
+     * @return True on success, false on error
1273
+     */
1274
+    public static function folder_deactivate($folder)
1275
+    {
1276
+        // remove from temp subscriptions, really?
1277
+        self::folder_unsubscribe($folder, true);
1278
+
1279
+        return self::set_state($folder, false);
1280
+    }
1281
+
1282
+
1283
+    /**
1284
+     * Return list of active folders
1285
+     */
1286
+    private static function get_states()
1287
+    {
1288
+        if (self::$states !== null) {
1289
+            return self::$states;
1290
+        }
1291
+
1292
+        $rcube   = rcube::get_instance();
1293
+        $folders = $rcube->config->get('kolab_active_folders');
1294
+
1295
+        if ($folders !== null) {
1296
+            self::$states = !empty($folders) ? explode('**', $folders) : array();
1297
+        }
1298
+        // for backward-compatibility copy server-side subscriptions to activation states
1299
+        else {
1300
+            self::setup();
1301
+            if (self::$subscriptions === null) {
1302
+                self::$with_tempsubs = false;
1303
+                self::$subscriptions = self::$imap->list_folders_subscribed();
1304
+                self::$with_tempsubs = true;
1305
+            }
1306
+            self::$states = self::$subscriptions;
1307
+            $folders = implode(self::$states, '**');
1308
+            $rcube->user->save_prefs(array('kolab_active_folders' => $folders));
1309
+        }
1310
+
1311
+        return self::$states;
1312
+    }
1313
+
1314
+
1315
+    /**
1316
+     * Update list of active folders
1317
+     */
1318
+    private static function set_state($folder, $state)
1319
+    {
1320
+        self::get_states();
1321
+
1322
+        // update in-memory list
1323
+        $idx = array_search($folder, self::$states);
1324
+        if ($state && $idx === false) {
1325
+            self::$states[] = $folder;
1326
+        }
1327
+        else if (!$state && $idx !== false) {
1328
+            unset(self::$states[$idx]);
1329
+        }
1330
+
1331
+        // update user preferences
1332
+        $folders = implode(self::$states, '**');
1333
+        $rcube   = rcube::get_instance();
1334
+        return $rcube->user->save_prefs(array('kolab_active_folders' => $folders));
1335
+    }
1336
+
1337
+    /**
1338
+     * Creates default folder of specified type
1339
+     * To be run when none of subscribed folders (of specified type) is found
1340
+     *
1341
+     * @param string $type  Folder type
1342
+     * @param string $props Folder properties (color, etc)
1343
+     *
1344
+     * @return string Folder name
1345
+     */
1346
+    public static function create_default_folder($type, $props = array())
1347
+    {
1348
+        if (!self::setup()) {
1349
+            return;
1350
+        }
1351
+
1352
+        $folders = self::$imap->get_metadata('*', array(kolab_storage::CTYPE_KEY_PRIVATE));
1353
+
1354
+        // from kolab_folders config
1355
+        $folder_type  = strpos($type, '.') ? str_replace('.', '_', $type) : $type . '_default';
1356
+        $default_name = self::$config->get('kolab_folders_' . $folder_type);
1357
+        $folder_type  = str_replace('_', '.', $folder_type);
1358
+
1359
+        // check if we have any folder in personal namespace
1360
+        // folder(s) may exist but not subscribed
1361
+        foreach ((array)$folders as $f => $data) {
1362
+            if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) {
1363
+                $folder = $f;
1364
+                break;
1365
+            }
1366
+        }
1367
+
1368
+        if (!$folder) {
1369
+            if (!$default_name) {
1370
+                $default_name = self::$default_folders[$type];
1371
+            }
1372
+
1373
+            if (!$default_name) {
1374
+                return;
1375
+            }
1376
+
1377
+            $folder = rcube_charset::convert($default_name, RCUBE_CHARSET, 'UTF7-IMAP');
1378
+            $prefix = self::$imap->get_namespace('prefix');
1379
+
1380
+            // add personal namespace prefix if needed
1381
+            if ($prefix && strpos($folder, $prefix) !== 0 && $folder != 'INBOX') {
1382
+                $folder = $prefix . $folder;
1383
+            }
1384
+
1385
+            if (!self::$imap->folder_exists($folder)) {
1386
+                if (!self::$imap->create_folder($folder)) {
1387
+                    return;
1388
+                }
1389
+            }
1390
+
1391
+            self::set_folder_type($folder, $folder_type);
1392
+        }
1393
+
1394
+        self::folder_subscribe($folder);
1395
+
1396
+        if ($props['active']) {
1397
+            self::set_state($folder, true);
1398
+        }
1399
+
1400
+        if (!empty($props)) {
1401
+            self::set_folder_props($folder, $props);
1402
+        }
1403
+
1404
+        return $folder;
1405
+    }
1406
+
1407
+    /**
1408
+     * Sets folder metadata properties
1409
+     *
1410
+     * @param string $folder Folder name
1411
+     * @param array  $prop   Folder properties
1412
+     */
1413
+    public static function set_folder_props($folder, &$prop)
1414
+    {
1415
+        if (!self::setup()) {
1416
+            return;
1417
+        }
1418
+
1419
+        // TODO: also save 'showalarams' and other properties here
1420
+        $ns        = self::$imap->folder_namespace($folder);
1421
+        $supported = array(
1422
+            'color'       => array(self::COLOR_KEY_SHARED, self::COLOR_KEY_PRIVATE),
1423
+            'displayname' => array(self::NAME_KEY_SHARED, self::NAME_KEY_PRIVATE),
1424
+        );
1425
+
1426
+        foreach ($supported as $key => $metakeys) {
1427
+            if (array_key_exists($key, $prop)) {
1428
+                $meta_saved = false;
1429
+                if ($ns == 'personal')  // save in shared namespace for personal folders
1430
+                    $meta_saved = self::$imap->set_metadata($folder, array($metakeys[0] => $prop[$key]));
1431
+                if (!$meta_saved)    // try in private namespace
1432
+                    $meta_saved = self::$imap->set_metadata($folder, array($metakeys[1] => $prop[$key]));
1433
+                if ($meta_saved)
1434
+                    unset($prop[$key]);  // unsetting will prevent fallback to local user prefs
1435
+            }
1436
+        }
1437
+    }
1438
+
1439
+
1440
+    /**
1441
+     *
1442
+     * @param mixed   $query    Search value (or array of field => value pairs)
1443
+     * @param int     $mode     Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
1444
+     * @param array   $required List of fields that shall ot be empty
1445
+     * @param int     $limit    Maximum number of records
1446
+     * @param int     $count    Returns the number of records found
1447
+     *
1448
+     * @return array List or false on error
1449
+     */
1450
+    public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0)
1451
+    {
1452
+        $query = str_replace('*', '', $query);
1453
+
1454
+        // requires a working LDAP setup
1455
+        if (!self::ldap() || strlen($query) == 0) {
1456
+            return array();
1457
+        }
1458
+
1459
+        // search users using the configured attributes
1460
+        $results = self::$ldap->dosearch(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit, $count);
1461
+
1462
+        // exclude myself
1463
+        if ($_SESSION['kolab_dn']) {
1464
+            unset($results[$_SESSION['kolab_dn']]);
1465
+        }
1466
+
1467
+        // resolve to IMAP folder name
1468
+        $root = self::namespace_root('other');
1469
+        $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
1470
+
1471
+        array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
1472
+            list($localpart, ) = explode('@', $user[$user_attrib]);
1473
+            $user['kolabtargetfolder'] = $root . $localpart;
1474
+        });
1475
+
1476
+        return $results;
1477
+    }
1478
+
1479
+
1480
+    /**
1481
+     * Returns a list of IMAP folders shared by the given user
1482
+     *
1483
+     * @param array   User entry from LDAP
1484
+     * @param string  Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
1485
+     * @param boolean Return subscribed folders only (null to use configured subscription mode)
1486
+     * @param array   Will be filled with folder-types data
1487
+     *
1488
+     * @return array List of folders
1489
+     */
1490
+    public static function list_user_folders($user, $type, $subscribed = null, &$folderdata = array())
1491
+    {
1492
+        self::setup();
1493
+
1494
+        $folders = array();
1495
+
1496
+        // use localpart of user attribute as root for folder listing
1497
+        $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
1498
+        if (!empty($user[$user_attrib])) {
1499
+            list($mbox) = explode('@', $user[$user_attrib]);
1500
+
1501
+            $delimiter = self::$imap->get_hierarchy_delimiter();
1502
+            $other_ns = self::namespace_root('other');
1503
+            $folders = self::list_folders($other_ns . $mbox . $delimiter, '*', $type, $subscribed, $folderdata);
1504
+        }
1505
+
1506
+        return $folders;
1507
+    }
1508
+
1509
+
1510
+    /**
1511
+     * Get a list of (virtual) top-level folders from the other users namespace
1512
+     *
1513
+     * @param string  Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
1514
+     * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
1515
+     *
1516
+     * @return array List of kolab_storage_folder_user objects
1517
+     */
1518
+    public static function get_user_folders($type, $subscribed)
1519
+    {
1520
+        $folders = $folderdata = array();
1521
+
1522
+        if (self::setup()) {
1523
+            $delimiter = self::$imap->get_hierarchy_delimiter();
1524
+            $other_ns = rtrim(self::namespace_root('other'), $delimiter);
1525
+            $path_len = count(explode($delimiter, $other_ns));
1526
+
1527
+            foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) {
1528
+                if ($foldername == 'INBOX')  // skip INBOX which is added by default
1529
+                    continue;
1530
+
1531
+                $path = explode($delimiter, $foldername);
1532
+
1533
+                // compare folder type if a subfolder is listed
1534
+                if ($type && count($path) > $path_len + 1 && $type != self::folder_type($foldername)) {
1535
+                    continue;
1536
+                }
1537
+
1538
+                // truncate folder path to top-level folders of the 'other' namespace
1539
+                $foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
1540
+
1541
+                if (!$folders[$foldername]) {
1542
+                    $folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
1543
+                }
1544
+            }
1545
+
1546
+            // for every (subscribed) user folder, list all (unsubscribed) subfolders
1547
+            foreach ($folders as $userfolder) {
1548
+                foreach ((array)self::list_folders($userfolder->name . $delimiter, '*', $type, false, $folderdata) as $foldername) {
1549
+                    if (!$folders[$foldername]) {
1550
+                        $folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
1551
+                        $userfolder->children[] = $folders[$foldername];
1552
+                    }
1553
+                }
1554
+            }
1555
+        }
1556
+
1557
+        return $folders;
1558
+    }
1559
+
1560
+
1561
+    /**
1562
+     * Handler for user_delete plugin hooks
1563
+     *
1564
+     * Remove all cache data from the local database related to the given user.
1565
+     */
1566
+    public static function delete_user_folders($args)
1567
+    {
1568
+        $db = rcmail::get_instance()->get_dbh();
1569
+        $prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
1570
+        $db->query("DELETE FROM " . $db->table_name('kolab_folders', true) . " WHERE `resource` LIKE ?", $prefix);
1571
+    }
1572
+}
1573
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php Added
1137
 
1
@@ -0,0 +1,1135 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab storage cache class providing a local caching layer for Kolab groupware objects.
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2012-2013, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+
26
+class kolab_storage_cache
27
+{
28
+    const DB_DATE_FORMAT = 'Y-m-d H:i:s';
29
+
30
+    public $sync_complete = false;
31
+
32
+    protected $db;
33
+    protected $imap;
34
+    protected $folder;
35
+    protected $uid2msg;
36
+    protected $objects;
37
+    protected $metadata = array();
38
+    protected $folder_id;
39
+    protected $resource_uri;
40
+    protected $enabled = true;
41
+    protected $synched = false;
42
+    protected $synclock = false;
43
+    protected $ready = false;
44
+    protected $cache_table;
45
+    protected $folders_table;
46
+    protected $max_sql_packet;
47
+    protected $max_sync_lock_time = 600;
48
+    protected $binary_items = array();
49
+    protected $extra_cols = array();
50
+    protected $order_by = null;
51
+    protected $limit = null;
52
+    protected $error = 0;
53
+
54
+
55
+    /**
56
+     * Factory constructor
57
+     */
58
+    public static function factory(kolab_storage_folder $storage_folder)
59
+    {
60
+        $subclass = 'kolab_storage_cache_' . $storage_folder->type;
61
+        if (class_exists($subclass)) {
62
+            return new $subclass($storage_folder);
63
+        }
64
+        else {
65
+            rcube::raise_error(array(
66
+                'code' => 900,
67
+                'type' => 'php',
68
+                'message' => "No kolab_storage_cache class found for folder '$storage_folder->name' of type '$storage_folder->type'"
69
+            ), true);
70
+
71
+            return new kolab_storage_cache($storage_folder);
72
+        }
73
+    }
74
+
75
+
76
+    /**
77
+     * Default constructor
78
+     */
79
+    public function __construct(kolab_storage_folder $storage_folder = null)
80
+    {
81
+        $rcmail = rcube::get_instance();
82
+        $this->db = $rcmail->get_dbh();
83
+        $this->imap = $rcmail->get_storage();
84
+        $this->enabled = $rcmail->config->get('kolab_cache', false);
85
+        $this->folders_table = $this->db->table_name('kolab_folders');
86
+
87
+        if ($this->enabled) {
88
+            // always read folder cache and lock state from DB master
89
+            $this->db->set_table_dsn('kolab_folders', 'w');
90
+            // remove sync-lock on script termination
91
+            $rcmail->add_shutdown_function(array($this, '_sync_unlock'));
92
+        }
93
+
94
+        if ($storage_folder)
95
+            $this->set_folder($storage_folder);
96
+    }
97
+
98
+    /**
99
+     * Direct access to cache by folder_id
100
+     * (only for internal use)
101
+     */
102
+    public function select_by_id($folder_id)
103
+    {
104
+        $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT * FROM `{$this->folders_table}` WHERE `folder_id` = ?", $folder_id));
105
+        if ($sql_arr) {
106
+            $this->metadata = $sql_arr;
107
+            $this->folder_id = $sql_arr['folder_id'];
108
+            $this->folder = new StdClass;
109
+            $this->folder->type = $sql_arr['type'];
110
+            $this->resource_uri = $sql_arr['resource'];
111
+            $this->cache_table = $this->db->table_name('kolab_cache_' . $sql_arr['type']);
112
+            $this->ready = true;
113
+        }
114
+    }
115
+
116
+    /**
117
+     * Connect cache with a storage folder
118
+     *
119
+     * @param kolab_storage_folder The storage folder instance to connect with
120
+     */
121
+    public function set_folder(kolab_storage_folder $storage_folder)
122
+    {
123
+        $this->folder = $storage_folder;
124
+
125
+        if (empty($this->folder->name) || !$this->folder->valid) {
126
+            $this->ready = false;
127
+            return;
128
+        }
129
+
130
+        // compose fully qualified ressource uri for this instance
131
+        $this->resource_uri = $this->folder->get_resource_uri();
132
+        $this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type);
133
+        $this->ready = $this->enabled && !empty($this->folder->type);
134
+        $this->folder_id = null;
135
+    }
136
+
137
+    /**
138
+     * Returns true if this cache supports query by type
139
+     */
140
+    public function has_type_col()
141
+    {
142
+        return in_array('type', $this->extra_cols);
143
+    }
144
+
145
+    /**
146
+     * Getter for the numeric ID used in cache tables
147
+     */
148
+    public function get_folder_id()
149
+    {
150
+        $this->_read_folder_data();
151
+        return $this->folder_id;
152
+    }
153
+
154
+    /**
155
+     * Returns code of last error
156
+     *
157
+     * @return int Error code
158
+     */
159
+    public function get_error()
160
+    {
161
+        return $this->error;
162
+    }
163
+
164
+    /**
165
+     * Synchronize local cache data with remote
166
+     */
167
+    public function synchronize()
168
+    {
169
+        // only sync once per request cycle
170
+        if ($this->synched)
171
+            return;
172
+
173
+        // increase time limit
174
+        @set_time_limit($this->max_sync_lock_time - 60);
175
+
176
+        // get effective time limit we have for synchronization (~70% of the execution time)
177
+        $time_limit = ini_get('max_execution_time') * 0.7;
178
+        $sync_start = time();
179
+
180
+        // assume sync will be completed
181
+        $this->sync_complete = true;
182
+
183
+        if (!$this->ready) {
184
+            // kolab cache is disabled, synchronize IMAP mailbox cache only
185
+            $this->imap->folder_sync($this->folder->name);
186
+        }
187
+        else {
188
+            // read cached folder metadata
189
+            $this->_read_folder_data();
190
+
191
+            // check cache status hash first ($this->metadata is set in _read_folder_data())
192
+            if ($this->metadata['ctag'] != $this->folder->get_ctag()) {
193
+                // lock synchronization for this folder or wait if locked
194
+                $this->_sync_lock();
195
+
196
+                // disable messages cache if configured to do so
197
+                $this->bypass(true);
198
+
199
+                // synchronize IMAP mailbox cache
200
+                $this->imap->folder_sync($this->folder->name);
201
+
202
+                // compare IMAP index with object cache index
203
+                $imap_index = $this->imap->index($this->folder->name, null, null, true, true);
204
+
205
+                // determine objects to fetch or to invalidate
206
+                if (!$imap_index->is_error()) {
207
+                    $imap_index = $imap_index->get();
208
+
209
+                    // read cache index
210
+                    $sql_result = $this->db->query(
211
+                        "SELECT `msguid`, `uid` FROM `{$this->cache_table}` WHERE `folder_id` = ?",
212
+                        $this->folder_id
213
+                    );
214
+
215
+                    $old_index = array();
216
+                    while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
217
+                        $old_index[] = $sql_arr['msguid'];
218
+                    }
219
+
220
+                    // fetch new objects from imap
221
+                    $i = 0;
222
+                    foreach (array_diff($imap_index, $old_index) as $msguid) {
223
+                        if ($object = $this->folder->read_object($msguid, '*')) {
224
+                            $this->_extended_insert($msguid, $object);
225
+
226
+                            // check time limit and abort sync if running too long
227
+                            if (++$i % 50 == 0 && time() - $sync_start > $time_limit) {
228
+                                $this->sync_complete = false;
229
+                                break;
230
+                            }
231
+                        }
232
+                    }
233
+                    $this->_extended_insert(0, null);
234
+
235
+                    // delete invalid entries from local DB
236
+                    $del_index = array_diff($old_index, $imap_index);
237
+                    if (!empty($del_index)) {
238
+                        $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
239
+                        $this->db->query(
240
+                            "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` IN ($quoted_ids)",
241
+                            $this->folder_id
242
+                        );
243
+                    }
244
+
245
+                    // update ctag value (will be written to database in _sync_unlock())
246
+                    if ($this->sync_complete) {
247
+                        $this->metadata['ctag'] = $this->folder->get_ctag();
248
+                    }
249
+                }
250
+
251
+                $this->bypass(false);
252
+
253
+                // remove lock
254
+                $this->_sync_unlock();
255
+            }
256
+        }
257
+
258
+        $this->check_error();
259
+        $this->synched = time();
260
+    }
261
+
262
+
263
+    /**
264
+     * Read a single entry from cache or from IMAP directly
265
+     *
266
+     * @param string Related IMAP message UID
267
+     * @param string Object type to read
268
+     * @param string IMAP folder name the entry relates to
269
+     * @param array  Hash array with object properties or null if not found
270
+     */
271
+    public function get($msguid, $type = null, $foldername = null)
272
+    {
273
+        // delegate to another cache instance
274
+        if ($foldername && $foldername != $this->folder->name) {
275
+            $success = false;
276
+            if ($targetfolder = kolab_storage::get_folder($foldername)) {
277
+                $success = $targetfolder->cache->get($msguid, $type);
278
+                $this->error = $targetfolder->cache->get_error();
279
+            }
280
+            return $success;
281
+        }
282
+
283
+        // load object if not in memory
284
+        if (!isset($this->objects[$msguid])) {
285
+            if ($this->ready) {
286
+                $this->_read_folder_data();
287
+
288
+                $sql_result = $this->db->query(
289
+                    "SELECT * FROM `{$this->cache_table}` ".
290
+                    "WHERE `folder_id` = ? AND `msguid` = ?",
291
+                    $this->folder_id,
292
+                    $msguid
293
+                );
294
+
295
+                if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
296
+                    $this->objects = array($msguid => $this->_unserialize($sql_arr));  // store only this object in memory (#2827)
297
+                }
298
+            }
299
+
300
+            // fetch from IMAP if not present in cache
301
+            if (empty($this->objects[$msguid])) {
302
+                if ($object = $this->folder->read_object($msguid, $type ?: '*', $foldername)) {
303
+                    $this->objects = array($msguid => $object);
304
+                    $this->set($msguid, $object);
305
+                }
306
+            }
307
+        }
308
+
309
+        $this->check_error();
310
+        return $this->objects[$msguid];
311
+    }
312
+
313
+
314
+    /**
315
+     * Insert/Update a cache entry
316
+     *
317
+     * @param string Related IMAP message UID
318
+     * @param mixed  Hash array with object properties to save or false to delete the cache entry
319
+     * @param string IMAP folder name the entry relates to
320
+     */
321
+    public function set($msguid, $object, $foldername = null)
322
+    {
323
+        if (!$msguid) {
324
+            return;
325
+        }
326
+
327
+        // delegate to another cache instance
328
+        if ($foldername && $foldername != $this->folder->name) {
329
+          if ($targetfolder = kolab_storage::get_folder($foldername)) {
330
+              $targetfolder->cache->set($msguid, $object);
331
+              $this->error = $targetfolder->cache->get_error();
332
+          }
333
+          return;
334
+        }
335
+
336
+        // remove old entry
337
+        if ($this->ready) {
338
+            $this->_read_folder_data();
339
+            $this->db->query("DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` = ?",
340
+                $this->folder_id, $msguid);
341
+        }
342
+
343
+        if ($object) {
344
+            // insert new object data...
345
+            $this->save($msguid, $object);
346
+        }
347
+        else {
348
+            // ...or set in-memory cache to false
349
+            $this->objects[$msguid] = $object;
350
+        }
351
+
352
+        $this->check_error();
353
+    }
354
+
355
+
356
+    /**
357
+     * Insert (or update) a cache entry
358
+     *
359
+     * @param int    Related IMAP message UID
360
+     * @param mixed  Hash array with object properties to save or false to delete the cache entry
361
+     * @param int    Optional old message UID (for update)
362
+     */
363
+    public function save($msguid, $object, $olduid = null)
364
+    {
365
+        // write to cache
366
+        if ($this->ready) {
367
+            $this->_read_folder_data();
368
+
369
+            $sql_data = $this->_serialize($object);
370
+            $sql_data['folder_id'] = $this->folder_id;
371
+            $sql_data['msguid']    = $msguid;
372
+            $sql_data['uid']       = $object['uid'];
373
+
374
+            $args = array();
375
+            $cols = array('folder_id', 'msguid', 'uid', 'changed', 'data', 'xml', 'tags', 'words');
376
+            $cols = array_merge($cols, $this->extra_cols);
377
+
378
+            foreach ($cols as $idx => $col) {
379
+                $cols[$idx] = $this->db->quote_identifier($col);
380
+                $args[]     = $sql_data[$col];
381
+            }
382
+
383
+            if ($olduid) {
384
+                foreach ($cols as $idx => $col) {
385
+                    $cols[$idx] = "$col = ?";
386
+                }
387
+
388
+                $query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols)
389
+                    . " WHERE `folder_id` = ? AND `msguid` = ?";
390
+                $args[] = $this->folder_id;
391
+                $args[] = $olduid;
392
+            }
393
+            else {
394
+                $query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols)
395
+                    . ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")";
396
+            }
397
+
398
+            $result = $this->db->query($query, $args);
399
+
400
+            if (!$this->db->affected_rows($result)) {
401
+                rcube::raise_error(array(
402
+                    'code' => 900, 'type' => 'php',
403
+                    'message' => "Failed to write to kolab cache"
404
+                ), true);
405
+            }
406
+        }
407
+
408
+        // keep a copy in memory for fast access
409
+        $this->objects = array($msguid => $object);
410
+        $this->uid2msg = array($object['uid'] => $msguid);
411
+
412
+        $this->check_error();
413
+    }
414
+
415
+
416
+    /**
417
+     * Move an existing cache entry to a new resource
418
+     *
419
+     * @param string Entry's IMAP message UID
420
+     * @param string Entry's Object UID
421
+     * @param object kolab_storage_folder Target storage folder instance
422
+     */
423
+    public function move($msguid, $uid, $target)
424
+    {
425
+        if ($this->ready) {
426
+            // clear cached uid mapping and force new lookup
427
+            unset($target->cache->uid2msg[$uid]);
428
+
429
+            // resolve new message UID in target folder
430
+            if ($new_msguid = $target->cache->uid2msguid($uid)) {
431
+                $this->_read_folder_data();
432
+
433
+                $this->db->query(
434
+                    "UPDATE `{$this->cache_table}` SET `folder_id` = ?, `msguid` = ? ".
435
+                    "WHERE `folder_id` = ? AND `msguid` = ?",
436
+                    $target->cache->get_folder_id(),
437
+                    $new_msguid,
438
+                    $this->folder_id,
439
+                    $msguid
440
+                );
441
+
442
+                $result = $this->db->affected_rows();
443
+            }
444
+        }
445
+
446
+        if (empty($result)) {
447
+            // just clear cache entry
448
+            $this->set($msguid, false);
449
+        }
450
+
451
+        unset($this->uid2msg[$uid]);
452
+        $this->check_error();
453
+    }
454
+
455
+
456
+    /**
457
+     * Remove all objects from local cache
458
+     */
459
+    public function purge()
460
+    {
461
+        if (!$this->ready) {
462
+            return true;
463
+        }
464
+
465
+        $this->_read_folder_data();
466
+
467
+        $result = $this->db->query(
468
+            "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ?",
469
+            $this->folder_id
470
+        );
471
+
472
+        return $this->db->affected_rows($result);
473
+    }
474
+
475
+    /**
476
+     * Update resource URI for existing cache entries
477
+     *
478
+     * @param string Target IMAP folder to move it to
479
+     */
480
+    public function rename($new_folder)
481
+    {
482
+        if (!$this->ready) {
483
+            return;
484
+        }
485
+
486
+        if ($target = kolab_storage::get_folder($new_folder)) {
487
+            // resolve new message UID in target folder
488
+            $this->db->query(
489
+                "UPDATE `{$this->folders_table}` SET `resource` = ? ".
490
+                "WHERE `resource` = ?",
491
+                $target->get_resource_uri(),
492
+                $this->resource_uri
493
+            );
494
+
495
+            $this->check_error();
496
+        }
497
+        else {
498
+            $this->error = kolab_storage::ERROR_IMAP_CONN;
499
+        }
500
+    }
501
+
502
+    /**
503
+     * Select Kolab objects filtered by the given query
504
+     *
505
+     * @param array Pseudo-SQL query as list of filter parameter triplets
506
+     *   triplet: array('<colname>', '<comparator>', '<value>')
507
+     * @param boolean Set true to only return UIDs instead of complete objects
508
+     * @return array List of Kolab data objects (each represented as hash array) or UIDs
509
+     */
510
+    public function select($query = array(), $uids = false)
511
+    {
512
+        $result = $uids ? array() : new kolab_storage_dataset($this);
513
+
514
+        // read from local cache DB (assume it to be synchronized)
515
+        if ($this->ready) {
516
+            $this->_read_folder_data();
517
+
518
+            // fetch full object data on one query if a small result set is expected
519
+            $fetchall = !$uids && ($this->limit ? $this->limit[0] : $this->count($query)) < 500;
520
+            $sql_query = "SELECT " . ($fetchall ? '*' : '`msguid` AS `_msguid`, `uid`') . " FROM `{$this->cache_table}` ".
521
+                         "WHERE `folder_id` = ? " . $this->_sql_where($query);
522
+            if (!empty($this->order_by)) {
523
+                $sql_query .= ' ORDER BY ' . $this->order_by;
524
+            }
525
+            $sql_result = $this->limit ?
526
+                $this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
527
+                $this->db->query($sql_query, $this->folder_id);
528
+
529
+            if ($this->db->is_error($sql_result)) {
530
+                if ($uids) {
531
+                    return null;
532
+                }
533
+                $result->set_error(true);
534
+                return $result;
535
+            }
536
+
537
+            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
538
+                if ($uids) {
539
+                    $this->uid2msg[$sql_arr['uid']] = $sql_arr['_msguid'];
540
+                    $result[] = $sql_arr['uid'];
541
+                }
542
+                else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
543
+                    $result[] = $object;
544
+                }
545
+                else if (!$fetchall) {
546
+                    // only add msguid to dataset index
547
+                    $result[] = $sql_arr;
548
+                }
549
+            }
550
+        }
551
+        // use IMAP
552
+        else {
553
+            $filter = $this->_query2assoc($query);
554
+
555
+            if ($filter['type']) {
556
+                $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
557
+                $index  = $this->imap->search_once($this->folder->name, $search);
558
+            }
559
+            else {
560
+                $index = $this->imap->index($this->folder->name, null, null, true, true);
561
+            }
562
+
563
+            if ($index->is_error()) {
564
+                $this->check_error();
565
+                if ($uids) {
566
+                    return null;
567
+                }
568
+                $result->set_error(true);
569
+                return $result;
570
+            }
571
+
572
+            $index  = $index->get();
573
+            $result = $uids ? $index : $this->_fetch($index, $filter['type']);
574
+
575
+            // TODO: post-filter result according to query
576
+        }
577
+
578
+        // We don't want to cache big results in-memory, however
579
+        // if we select only one object here, there's a big chance we will need it later
580
+        if (!$uids && count($result) == 1) {
581
+            if ($msguid = $result[0]['_msguid']) {
582
+                $this->uid2msg[$result[0]['uid']] = $msguid;
583
+                $this->objects = array($msguid => $result[0]);
584
+            }
585
+        }
586
+
587
+        $this->check_error();
588
+
589
+        return $result;
590
+    }
591
+
592
+
593
+    /**
594
+     * Get number of objects mathing the given query
595
+     *
596
+     * @param array  $query Pseudo-SQL query as list of filter parameter triplets
597
+     * @return integer The number of objects of the given type
598
+     */
599
+    public function count($query = array())
600
+    {
601
+        // read from local cache DB (assume it to be synchronized)
602
+        if ($this->ready) {
603
+            $this->_read_folder_data();
604
+
605
+            $sql_result = $this->db->query(
606
+                "SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ".
607
+                "WHERE `folder_id` = ?" . $this->_sql_where($query),
608
+                $this->folder_id
609
+            );
610
+
611
+            if ($this->db->is_error($sql_result)) {
612
+                return null;
613
+            }
614
+
615
+            $sql_arr = $this->db->fetch_assoc($sql_result);
616
+            $count   = intval($sql_arr['numrows']);
617
+        }
618
+        // use IMAP
619
+        else {
620
+            $filter = $this->_query2assoc($query);
621
+
622
+            if ($filter['type']) {
623
+                $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
624
+                $index  = $this->imap->search_once($this->folder->name, $search);
625
+            }
626
+            else {
627
+                $index = $this->imap->index($this->folder->name, null, null, true, true);
628
+            }
629
+
630
+            if ($index->is_error()) {
631
+                $this->check_error();
632
+                return null;
633
+            }
634
+
635
+            // TODO: post-filter result according to query
636
+
637
+            $count = $index->count();
638
+        }
639
+
640
+        $this->check_error();
641
+        return $count;
642
+    }
643
+
644
+    /**
645
+     * Define ORDER BY clause for cache queries
646
+     */
647
+    public function set_order_by($sortcols)
648
+    {
649
+        if (!empty($sortcols)) {
650
+            $this->order_by = '`' . join('`, `', (array)$sortcols) . '`';
651
+        }
652
+        else {
653
+            $this->order_by = null;
654
+        }
655
+    }
656
+
657
+    /**
658
+     * Define LIMIT clause for cache queries
659
+     */
660
+    public function set_limit($length, $offset = 0)
661
+    {
662
+        $this->limit = array($length, $offset);
663
+    }
664
+
665
+    /**
666
+     * Helper method to compose a valid SQL query from pseudo filter triplets
667
+     */
668
+    protected function _sql_where($query)
669
+    {
670
+        $sql_where = '';
671
+        foreach ((array) $query as $param) {
672
+            if (is_array($param[0])) {
673
+                $subq = array();
674
+                foreach ($param[0] as $q) {
675
+                    $subq[] = preg_replace('/^\s*AND\s+/i', '', $this->_sql_where(array($q)));
676
+                }
677
+                if (!empty($subq)) {
678
+                    $sql_where .= ' AND (' . implode($param[1] == 'OR' ? ' OR ' : ' AND ', $subq) . ')';
679
+                }
680
+                continue;
681
+            }
682
+            else if ($param[1] == '=' && is_array($param[2])) {
683
+                $qvalue = '(' . join(',', array_map(array($this->db, 'quote'), $param[2])) . ')';
684
+                $param[1] = 'IN';
685
+            }
686
+            else if ($param[1] == '~' || $param[1] == 'LIKE' || $param[1] == '!~' || $param[1] == '!LIKE') {
687
+                $not = ($param[1] == '!~' || $param[1] == '!LIKE') ? 'NOT ' : '';
688
+                $param[1] = $not . 'LIKE';
689
+                $qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
690
+            }
691
+            else if ($param[0] == 'tags') {
692
+                $param[1] = ($param[1] == '!=' ? 'NOT ' : '' ) . 'LIKE';
693
+                $qvalue = $this->db->quote('% '.$param[2].' %');
694
+            }
695
+            else {
696
+                $qvalue = $this->db->quote($param[2]);
697
+            }
698
+
699
+            $sql_where .= sprintf(' AND %s %s %s',
700
+                $this->db->quote_identifier($param[0]),
701
+                $param[1],
702
+                $qvalue
703
+            );
704
+        }
705
+
706
+        return $sql_where;
707
+    }
708
+
709
+    /**
710
+     * Helper method to convert the given pseudo-query triplets into
711
+     * an associative filter array with 'equals' values only
712
+     */
713
+    protected function _query2assoc($query)
714
+    {
715
+        // extract object type from query parameter
716
+        $filter = array();
717
+        foreach ($query as $param) {
718
+            if ($param[1] == '=')
719
+                $filter[$param[0]] = $param[2];
720
+        }
721
+        return $filter;
722
+    }
723
+
724
+    /**
725
+     * Fetch messages from IMAP
726
+     *
727
+     * @param array  List of message UIDs to fetch
728
+     * @param string Requested object type or * for all
729
+     * @param string IMAP folder to read from
730
+     * @return array List of parsed Kolab objects
731
+     */
732
+    protected function _fetch($index, $type = null, $folder = null)
733
+    {
734
+        $results = new kolab_storage_dataset($this);
735
+        foreach ((array)$index as $msguid) {
736
+            if ($object = $this->folder->read_object($msguid, $type, $folder)) {
737
+                $results[] = $object;
738
+                $this->set($msguid, $object);
739
+            }
740
+        }
741
+
742
+        return $results;
743
+    }
744
+
745
+    /**
746
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
747
+     */
748
+    protected function _serialize($object)
749
+    {
750
+        $sql_data = array('changed' => null, 'xml' => '', 'tags' => '', 'words' => '');
751
+
752
+        if ($object['changed']) {
753
+            $sql_data['changed'] = date('Y-m-d H:i:s', is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']);
754
+        }
755
+
756
+        if ($object['_formatobj']) {
757
+            $sql_data['xml']   = preg_replace('!(</?[a-z0-9:-]+>)[\n\r\t\s]+!ms', '$1', (string)$object['_formatobj']->write(3.0));
758
+            $sql_data['tags']  = ' ' . join(' ', $object['_formatobj']->get_tags()) . ' ';  // pad with spaces for strict/prefix search
759
+            $sql_data['words'] = ' ' . join(' ', $object['_formatobj']->get_words()) . ' ';
760
+        }
761
+
762
+        // extract object data
763
+        $data = array();
764
+        foreach ($object as $key => $val) {
765
+            // skip empty properties
766
+            if ($val === "" || $val === null) {
767
+                continue;
768
+            }
769
+            // mark binary data to be extracted from xml on unserialize()
770
+            if (isset($this->binary_items[$key])) {
771
+                $data[$key] = true;
772
+            }
773
+            else if ($key[0] != '_') {
774
+                $data[$key] = $val;
775
+            }
776
+            else if ($key == '_attachments') {
777
+                foreach ($val as $k => $att) {
778
+                    unset($att['content'], $att['path']);
779
+                    if ($att['id'])
780
+                        $data[$key][$k] = $att;
781
+                }
782
+            }
783
+        }
784
+
785
+        // use base64 encoding (Bug #1912, #2662)
786
+        $sql_data['data'] = base64_encode(serialize($data));
787
+
788
+        return $sql_data;
789
+    }
790
+
791
+    /**
792
+     * Helper method to turn stored cache data into a valid storage object
793
+     */
794
+    protected function _unserialize($sql_arr)
795
+    {
796
+        // check if data is a base64-encoded string, for backward compat.
797
+        if (strpos(substr($sql_arr['data'], 0, 64), ':') === false) {
798
+            $sql_arr['data'] = base64_decode($sql_arr['data']);
799
+        }
800
+
801
+        $object = unserialize($sql_arr['data']);
802
+
803
+        // de-serialization failed
804
+        if ($object === false) {
805
+            rcube::raise_error(array(
806
+                'code' => 900, 'type' => 'php',
807
+                'message' => "Malformed data for {$this->resource_uri}/{$sql_arr['msguid']} object."
808
+            ), true);
809
+
810
+            return null;
811
+        }
812
+
813
+        // decode binary properties
814
+        foreach ($this->binary_items as $key => $regexp) {
815
+            if (!empty($object[$key]) && preg_match($regexp, $sql_arr['xml'], $m)) {
816
+                $object[$key] = base64_decode($m[1]);
817
+            }
818
+        }
819
+
820
+        $object_type = $sql_arr['type'] ?: $this->folder->type;
821
+        $format_type = $this->folder->type == 'configuration' ? 'configuration' : $object_type;
822
+
823
+        // add meta data
824
+        $object['_type']      = $object_type;
825
+        $object['_msguid']    = $sql_arr['msguid'];
826
+        $object['_mailbox']   = $this->folder->name;
827
+        $object['_size']      = strlen($sql_arr['xml']);
828
+        $object['_formatobj'] = kolab_format::factory($format_type, 3.0, $sql_arr['xml']);
829
+
830
+        return $object;
831
+    }
832
+
833
+    /**
834
+     * Write records into cache using extended inserts to reduce the number of queries to be executed
835
+     *
836
+     * @param int  Message UID. Set 0 to commit buffered inserts
837
+     * @param array Kolab object to cache
838
+     */
839
+    protected function _extended_insert($msguid, $object)
840
+    {
841
+        static $buffer = '';
842
+
843
+        $line = '';
844
+        if ($object) {
845
+            $sql_data = $this->_serialize($object);
846
+
847
+            // Skip multifolder insert for Oracle, we can't put long data inline
848
+            if ($this->db->db_provider == 'oracle') {
849
+                $extra_cols = '';
850
+                if ($this->extra_cols) {
851
+                    $extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols);
852
+                    $extra_cols = ', ' . join(', ', $extra_cols);
853
+                    $extra_args = str_repeat(', ?', count($this->extra_cols));
854
+                }
855
+
856
+                $params = array($this->folder_id, $msguid, $object['uid'], $sql_data['changed'],
857
+                    $sql_data['data'], $sql_data['xml'], $sql_data['tags'], $sql_data['words']);
858
+
859
+                foreach ($this->extra_cols as $col) {
860
+                    $params[] = $sql_data[$col];
861
+                }
862
+
863
+                $result = $this->db->query(
864
+                    "INSERT INTO `{$this->cache_table}` "
865
+                    . " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)"
866
+                    . " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_args)",
867
+                    $params
868
+                );
869
+
870
+                if (!$this->db->affected_rows($result)) {
871
+                    rcube::raise_error(array(
872
+                        'code' => 900, 'type' => 'php',
873
+                        'message' => "Failed to write to kolab cache"
874
+                    ), true);
875
+                }
876
+
877
+                return;
878
+            }
879
+
880
+            $values = array(
881
+                $this->db->quote($this->folder_id),
882
+                $this->db->quote($msguid),
883
+                $this->db->quote($object['uid']),
884
+                $this->db->now(),
885
+                $this->db->quote($sql_data['changed']),
886
+                $this->db->quote($sql_data['data']),
887
+                $this->db->quote($sql_data['xml']),
888
+                $this->db->quote($sql_data['tags']),
889
+                $this->db->quote($sql_data['words']),
890
+            );
891
+            foreach ($this->extra_cols as $col) {
892
+                $values[] = $this->db->quote($sql_data[$col]);
893
+            }
894
+            $line = '(' . join(',', $values) . ')';
895
+        }
896
+
897
+        if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
898
+            $extra_cols = '';
899
+            if ($this->extra_cols) {
900
+                $extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols);
901
+                $extra_cols = ', ' . join(', ', $extra_cols);
902
+            }
903
+
904
+            $result = $this->db->query(
905
+                "INSERT INTO `{$this->cache_table}` ".
906
+                " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)".
907
+                " VALUES $buffer"
908
+            );
909
+
910
+            if (!$this->db->affected_rows($result)) {
911
+                rcube::raise_error(array(
912
+                    'code' => 900, 'type' => 'php',
913
+                    'message' => "Failed to write to kolab cache"
914
+                ), true);
915
+            }
916
+
917
+            $buffer = '';
918
+        }
919
+
920
+        $buffer .= ($buffer ? ',' : '') . $line;
921
+    }
922
+
923
+    /**
924
+     * Returns max_allowed_packet from mysql config
925
+     */
926
+    protected function max_sql_packet()
927
+    {
928
+        if (!$this->max_sql_packet) {
929
+            // mysql limit or max 4 MB
930
+            $value = $this->db->get_variable('max_allowed_packet', 1048500);
931
+            $this->max_sql_packet = min($value, 4*1024*1024) - 2000;
932
+        }
933
+
934
+        return $this->max_sql_packet;
935
+    }
936
+
937
+    /**
938
+     * Read this folder's ID and cache metadata
939
+     */
940
+    protected function _read_folder_data()
941
+    {
942
+        // already done
943
+        if (!empty($this->folder_id) || !$this->ready)
944
+            return;
945
+
946
+        $sql_arr = $this->db->fetch_assoc($this->db->query(
947
+                "SELECT `folder_id`, `synclock`, `ctag`"
948
+                . " FROM `{$this->folders_table}` WHERE `resource` = ?",
949
+                $this->resource_uri
950
+        ));
951
+
952
+        if ($sql_arr) {
953
+            $this->metadata = $sql_arr;
954
+            $this->folder_id = $sql_arr['folder_id'];
955
+        }
956
+        else {
957
+            $this->db->query("INSERT INTO `{$this->folders_table}` (`resource`, `type`)"
958
+                . " VALUES (?, ?)", $this->resource_uri, $this->folder->type);
959
+
960
+            $this->folder_id = $this->db->insert_id('kolab_folders');
961
+            $this->metadata = array();
962
+        }
963
+    }
964
+
965
+    /**
966
+     * Check lock record for this folder and wait if locked or set lock
967
+     */
968
+    protected function _sync_lock()
969
+    {
970
+        if (!$this->ready)
971
+            return;
972
+
973
+        $this->_read_folder_data();
974
+
975
+        // abort if database is not set-up
976
+        if ($this->db->is_error()) {
977
+            $this->check_error();
978
+            $this->ready = false;
979
+            return;
980
+        }
981
+
982
+        $read_query  = "SELECT `synclock`, `ctag` FROM `{$this->folders_table}` WHERE `folder_id` = ?";
983
+        $write_query = "UPDATE `{$this->folders_table}` SET `synclock` = ? WHERE `folder_id` = ? AND `synclock` = ?";
984
+
985
+        // wait if locked (expire locks after 10 minutes) ...
986
+        // ... or if setting lock fails (another process meanwhile set it)
987
+        while (
988
+            (intval($this->metadata['synclock']) + $this->max_sync_lock_time > time()) ||
989
+            (($res = $this->db->query($write_query, time(), $this->folder_id, intval($this->metadata['synclock']))) &&
990
+                !($affected = $this->db->affected_rows($res)))
991
+        ) {
992
+            usleep(500000);
993
+            $this->metadata = $this->db->fetch_assoc($this->db->query($read_query, $this->folder_id));
994
+        }
995
+
996
+        $this->synclock = $affected > 0;
997
+    }
998
+
999
+    /**
1000
+     * Remove lock for this folder
1001
+     */
1002
+    public function _sync_unlock()
1003
+    {
1004
+        if (!$this->ready || !$this->synclock)
1005
+            return;
1006
+
1007
+        $this->db->query(
1008
+            "UPDATE `{$this->folders_table}` SET `synclock` = 0, `ctag` = ? WHERE `folder_id` = ?",
1009
+            $this->metadata['ctag'],
1010
+            $this->folder_id
1011
+        );
1012
+
1013
+        $this->synclock = false;
1014
+    }
1015
+
1016
+    /**
1017
+     * Check IMAP connection error state
1018
+     */
1019
+    protected function check_error()
1020
+    {
1021
+        if (($err_code = $this->imap->get_error_code()) < 0) {
1022
+            $this->error = kolab_storage::ERROR_IMAP_CONN;
1023
+            if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) {
1024
+                $this->error = kolab_storage::ERROR_NO_PERMISSION;
1025
+            }
1026
+        }
1027
+        else if ($this->db->is_error()) {
1028
+            $this->error = kolab_storage::ERROR_CACHE_DB;
1029
+        }
1030
+    }
1031
+
1032
+    /**
1033
+     * Resolve an object UID into an IMAP message UID
1034
+     *
1035
+     * @param string  Kolab object UID
1036
+     * @param boolean Include deleted objects
1037
+     * @return int The resolved IMAP message UID
1038
+     */
1039
+    public function uid2msguid($uid, $deleted = false)
1040
+    {
1041
+        // query local database if available
1042
+        if (!isset($this->uid2msg[$uid]) && $this->ready) {
1043
+            $this->_read_folder_data();
1044
+
1045
+            $sql_result = $this->db->query(
1046
+                "SELECT `msguid` FROM `{$this->cache_table}` ".
1047
+                "WHERE `folder_id` = ? AND `uid` = ? ORDER BY `msguid` DESC",
1048
+                $this->folder_id,
1049
+                $uid
1050
+            );
1051
+
1052
+            if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
1053
+                $this->uid2msg[$uid] = $sql_arr['msguid'];
1054
+            }
1055
+        }
1056
+
1057
+        if (!isset($this->uid2msg[$uid])) {
1058
+            // use IMAP SEARCH to get the right message
1059
+            $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') .
1060
+                'HEADER SUBJECT ' . rcube_imap_generic::escape($uid));
1061
+            $results = $index->get();
1062
+            $this->uid2msg[$uid] = end($results);
1063
+        }
1064
+
1065
+        return $this->uid2msg[$uid];
1066
+    }
1067
+
1068
+    /**
1069
+     * Getter for protected member variables
1070
+     */
1071
+    public function __get($name)
1072
+    {
1073
+        if ($name == 'folder_id') {
1074
+            $this->_read_folder_data();
1075
+        }
1076
+
1077
+        return $this->$name;
1078
+    }
1079
+
1080
+    /**
1081
+     * Bypass Roundcube messages cache.
1082
+     * Roundcube cache duplicates information already stored in kolab_cache.
1083
+     *
1084
+     * @param bool $disable True disables, False enables messages cache
1085
+     */
1086
+    public function bypass($disable = false)
1087
+    {
1088
+        // if kolab cache is disabled do nothing
1089
+        if (!$this->enabled) {
1090
+            return;
1091
+        }
1092
+
1093
+        static $messages_cache, $cache_bypass;
1094
+
1095
+        if ($messages_cache === null) {
1096
+            $rcmail = rcube::get_instance();
1097
+            $messages_cache = (bool) $rcmail->config->get('messages_cache');
1098
+            $cache_bypass   = (int) $rcmail->config->get('kolab_messages_cache_bypass');
1099
+        }
1100
+
1101
+        if ($messages_cache) {
1102
+            // handle recurrent (multilevel) bypass() calls
1103
+            if ($disable) {
1104
+                $this->cache_bypassed += 1;
1105
+                if ($this->cache_bypassed > 1) {
1106
+                    return;
1107
+                }
1108
+            }
1109
+            else {
1110
+                $this->cache_bypassed -= 1;
1111
+                if ($this->cache_bypassed > 0) {
1112
+                    return;
1113
+                }
1114
+            }
1115
+
1116
+            switch ($cache_bypass) {
1117
+                case 2:
1118
+                    // Disable messages cache completely
1119
+                    $this->imap->set_messages_caching(!$disable);
1120
+                    break;
1121
+
1122
+                case 1:
1123
+                    // We'll disable messages cache, but keep index cache.
1124
+                    // Default mode is both (MODE_INDEX | MODE_MESSAGE)
1125
+                    $mode = rcube_imap_cache::MODE_INDEX;
1126
+
1127
+                    if (!$disable) {
1128
+                        $mode |= rcube_imap_cache::MODE_MESSAGE;
1129
+                    }
1130
+
1131
+                    $this->imap->set_messages_caching(true, $mode);
1132
+            }
1133
+        }
1134
+    }
1135
+
1136
+}
1137
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php Added
90
 
1
@@ -0,0 +1,88 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab storage cache class for configuration objects
6
+ *
7
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
8
+ *
9
+ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
10
+ *
11
+ * This program is free software: you can redistribute it and/or modify
12
+ * it under the terms of the GNU Affero General Public License as
13
+ * published by the Free Software Foundation, either version 3 of the
14
+ * License, or (at your option) any later version.
15
+ *
16
+ * This program is distributed in the hope that it will be useful,
17
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ * GNU Affero General Public License for more details.
20
+ *
21
+ * You should have received a copy of the GNU Affero General Public License
22
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
+ */
24
+
25
+class kolab_storage_cache_configuration extends kolab_storage_cache
26
+{
27
+    protected $extra_cols = array('type');
28
+
29
+    /**
30
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
31
+     *
32
+     * @override
33
+     */
34
+    protected function _serialize($object)
35
+    {
36
+        $sql_data = parent::_serialize($object);
37
+        $sql_data['type'] = $object['type'];
38
+
39
+        return $sql_data;
40
+    }
41
+
42
+    /**
43
+     * Select Kolab objects filtered by the given query
44
+     *
45
+     * @param array Pseudo-SQL query as list of filter parameter triplets
46
+     * @param boolean Set true to only return UIDs instead of complete objects
47
+     * @return array List of Kolab data objects (each represented as hash array) or UIDs
48
+     */
49
+    public function select($query = array(), $uids = false)
50
+    {
51
+        // modify query for IMAP search: query param 'type' is actually a subtype
52
+        if (!$this->ready) {
53
+            foreach ($query as $i => $tuple) {
54
+                if ($tuple[0] == 'type') {
55
+                    $tuple[2] = 'configuration.' . $tuple[2];
56
+                    $query[$i] = $tuple;
57
+                }
58
+            }
59
+        }
60
+
61
+        return parent::select($query, $uids);
62
+    }
63
+
64
+    /**
65
+     * Helper method to compose a valid SQL query from pseudo filter triplets
66
+     */
67
+    protected function _sql_where($query)
68
+    {
69
+        if (is_array($query)) {
70
+            foreach ($query as $idx => $param) {
71
+                // convert category filter
72
+                if ($param[0] == 'category') {
73
+                    $param[2] = array_map(function($n) { return 'category:' . $n; }, (array) $param[2]);
74
+
75
+                    $query[$idx][0] = 'tags';
76
+                    $query[$idx][2] = count($param[2]) > 1 ? $param[2] : $param[2][0];
77
+                }
78
+                // convert member filter (we support only = operator with single value)
79
+                else if ($param[0] == 'member') {
80
+                    $query[$idx][0] = 'words';
81
+                    $query[$idx][1] = '~';
82
+                    $query[$idx][2] = '^' . $param[2] . '$';
83
+                }
84
+            }
85
+        }
86
+
87
+        return parent::_sql_where($query);
88
+    }
89
+}
90
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_contact.php Added
62
 
1
@@ -0,0 +1,59 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab storage cache class for contact objects
6
+ *
7
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
8
+ *
9
+ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
10
+ *
11
+ * This program is free software: you can redistribute it and/or modify
12
+ * it under the terms of the GNU Affero General Public License as
13
+ * published by the Free Software Foundation, either version 3 of the
14
+ * License, or (at your option) any later version.
15
+ *
16
+ * This program is distributed in the hope that it will be useful,
17
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ * GNU Affero General Public License for more details.
20
+ *
21
+ * You should have received a copy of the GNU Affero General Public License
22
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
+ */
24
+
25
+class kolab_storage_cache_contact extends kolab_storage_cache
26
+{
27
+    protected $extra_cols = array('type','name','firstname','surname','email');
28
+    protected $binary_items = array(
29
+        'photo'          => '|<photo><uri>[^;]+;base64,([^<]+)</uri></photo>|i',
30
+        'pgppublickey'   => '|<key><uri>date:application/pgp-keys;base64,([^<]+)</uri></key>|i',
31
+        'pkcs7publickey' => '|<key><uri>date:application/pkcs7-mime;base64,([^<]+)</uri></key>|i',
32
+    );
33
+
34
+    /**
35
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
36
+     *
37
+     * @override
38
+     */
39
+    protected function _serialize($object)
40
+    {
41
+        $sql_data = parent::_serialize($object);
42
+        $sql_data['type'] = $object['_type'];
43
+
44
+        // columns for sorting
45
+        $sql_data['name']      = rcube_charset::clean($object['name'] . $object['prefix']);
46
+        $sql_data['firstname'] = rcube_charset::clean($object['firstname'] . $object['middlename'] . $object['surname']);
47
+        $sql_data['surname']   = rcube_charset::clean($object['surname']   . $object['firstname']  . $object['middlename']);
48
+        $sql_data['email']     = rcube_charset::clean(is_array($object['email']) ? $object['email'][0] : $object['email']);
49
+
50
+        if (is_array($sql_data['email'])) {
51
+            $sql_data['email'] = $sql_data['email']['address'];
52
+        }
53
+        // avoid value being null
54
+        if (empty($sql_data['email'])) {
55
+            $sql_data['email'] = '';
56
+        }
57
+
58
+        return $sql_data;
59
+    }
60
+}
61
\ No newline at end of file
62
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_event.php Added
52
 
1
@@ -0,0 +1,49 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab storage cache class for calendar event objects
6
+ *
7
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
8
+ *
9
+ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
10
+ *
11
+ * This program is free software: you can redistribute it and/or modify
12
+ * it under the terms of the GNU Affero General Public License as
13
+ * published by the Free Software Foundation, either version 3 of the
14
+ * License, or (at your option) any later version.
15
+ *
16
+ * This program is distributed in the hope that it will be useful,
17
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ * GNU Affero General Public License for more details.
20
+ *
21
+ * You should have received a copy of the GNU Affero General Public License
22
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
+ */
24
+
25
+class kolab_storage_cache_event extends kolab_storage_cache
26
+{
27
+    protected $extra_cols = array('dtstart','dtend');
28
+
29
+    /**
30
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
31
+     *
32
+     * @override
33
+     */
34
+    protected function _serialize($object)
35
+    {
36
+        $sql_data = parent::_serialize($object);
37
+
38
+        $sql_data['dtstart'] = is_object($object['start']) ? $object['start']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['start']);
39
+        $sql_data['dtend']   = is_object($object['end'])   ? $object['end']->format(self::DB_DATE_FORMAT)   : date(self::DB_DATE_FORMAT, $object['end']);
40
+
41
+        // extend date range for recurring events
42
+        if ($object['recurrence'] && $object['_formatobj']) {
43
+            $recurrence = new kolab_date_recurrence($object['_formatobj']);
44
+            $dtend = $recurrence->end() ?: new DateTime('now +10 years');
45
+            $sql_data['dtend'] = $dtend->format(self::DB_DATE_FORMAT);
46
+        }
47
+
48
+        return $sql_data;
49
+    }
50
+}
51
\ No newline at end of file
52
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_file.php Added
47
 
1
@@ -0,0 +1,44 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab storage cache class for file objects
6
+ *
7
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
8
+ *
9
+ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
10
+ *
11
+ * This program is free software: you can redistribute it and/or modify
12
+ * it under the terms of the GNU Affero General Public License as
13
+ * published by the Free Software Foundation, either version 3 of the
14
+ * License, or (at your option) any later version.
15
+ *
16
+ * This program is distributed in the hope that it will be useful,
17
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ * GNU Affero General Public License for more details.
20
+ *
21
+ * You should have received a copy of the GNU Affero General Public License
22
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
+ */
24
+
25
+class kolab_storage_cache_file extends kolab_storage_cache
26
+{
27
+    protected $extra_cols = array('filename');
28
+
29
+    /**
30
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
31
+     *
32
+     * @override
33
+     */
34
+    protected function _serialize($object)
35
+    {
36
+        $sql_data = parent::_serialize($object);
37
+
38
+        if (!empty($object['_attachments'])) {
39
+            reset($object['_attachments']);
40
+            $sql_data['filename'] = $object['_attachments'][key($object['_attachments'])]['name'];
41
+        }
42
+
43
+        return $sql_data;
44
+    }
45
+}
46
\ No newline at end of file
47
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_freebusy.php Added
30
 
1
@@ -0,0 +1,27 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab storage cache class for freebusy objects
6
+ *
7
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
8
+ *
9
+ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
10
+ *
11
+ * This program is free software: you can redistribute it and/or modify
12
+ * it under the terms of the GNU Affero General Public License as
13
+ * published by the Free Software Foundation, either version 3 of the
14
+ * License, or (at your option) any later version.
15
+ *
16
+ * This program is distributed in the hope that it will be useful,
17
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ * GNU Affero General Public License for more details.
20
+ *
21
+ * You should have received a copy of the GNU Affero General Public License
22
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
+ */
24
+
25
+class kolab_storage_cache_freebusy extends kolab_storage_cache
26
+{
27
+        protected $extra_cols = array('dtstart','dtend');
28
+}
29
\ No newline at end of file
30
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_journal.php Added
31
 
1
@@ -0,0 +1,28 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab storage cache class for journal objects
6
+ *
7
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
8
+ *
9
+ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
10
+ *
11
+ * This program is free software: you can redistribute it and/or modify
12
+ * it under the terms of the GNU Affero General Public License as
13
+ * published by the Free Software Foundation, either version 3 of the
14
+ * License, or (at your option) any later version.
15
+ *
16
+ * This program is distributed in the hope that it will be useful,
17
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ * GNU Affero General Public License for more details.
20
+ *
21
+ * You should have received a copy of the GNU Affero General Public License
22
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
+ */
24
+
25
+class kolab_storage_cache_journal extends kolab_storage_cache
26
+{
27
+    protected $extra_cols = array('dtstart','dtend');
28
+    
29
+}
30
\ No newline at end of file
31
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_mongodb.php Added
563
 
1
@@ -0,0 +1,561 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab storage cache class providing a local caching layer for Kolab groupware objects.
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+
26
+class kolab_storage_cache_mongodb
27
+{
28
+    private $db;
29
+    private $imap;
30
+    private $folder;
31
+    private $uid2msg;
32
+    private $objects;
33
+    private $index = array();
34
+    private $resource_uri;
35
+    private $enabled = true;
36
+    private $synched = false;
37
+    private $synclock = false;
38
+    private $ready = false;
39
+    private $max_sql_packet = 1046576;  // 1 MB - 2000 bytes
40
+    private $binary_cols = array('photo','pgppublickey','pkcs7publickey');
41
+
42
+
43
+    /**
44
+     * Default constructor
45
+     */
46
+    public function __construct(kolab_storage_folder $storage_folder = null)
47
+    {
48
+        $rcmail = rcube::get_instance();
49
+        $mongo = new Mongo();
50
+        $this->db = $mongo->kolab_cache;
51
+        $this->imap = $rcmail->get_storage();
52
+        $this->enabled = $rcmail->config->get('kolab_cache', false);
53
+
54
+        if ($this->enabled) {
55
+            // remove sync-lock on script termination
56
+            $rcmail->add_shutdown_function(array($this, '_sync_unlock'));
57
+        }
58
+
59
+        if ($storage_folder)
60
+            $this->set_folder($storage_folder);
61
+    }
62
+
63
+
64
+    /**
65
+     * Connect cache with a storage folder
66
+     *
67
+     * @param kolab_storage_folder The storage folder instance to connect with
68
+     */
69
+    public function set_folder(kolab_storage_folder $storage_folder)
70
+    {
71
+        $this->folder = $storage_folder;
72
+
73
+        if (empty($this->folder->name)) {
74
+            $this->ready = false;
75
+            return;
76
+        }
77
+
78
+        // compose fully qualified ressource uri for this instance
79
+        $this->resource_uri = $this->folder->get_resource_uri();
80
+        $this->ready = $this->enabled;
81
+    }
82
+
83
+
84
+    /**
85
+     * Synchronize local cache data with remote
86
+     */
87
+    public function synchronize()
88
+    {
89
+        // only sync once per request cycle
90
+        if ($this->synched)
91
+            return;
92
+
93
+        // increase time limit
94
+        @set_time_limit(500);
95
+
96
+        // lock synchronization for this folder or wait if locked
97
+        $this->_sync_lock();
98
+
99
+        // synchronize IMAP mailbox cache
100
+        $this->imap->folder_sync($this->folder->name);
101
+
102
+        // compare IMAP index with object cache index
103
+        $imap_index = $this->imap->index($this->folder->name);
104
+        $this->index = $imap_index->get();
105
+
106
+        // determine objects to fetch or to invalidate
107
+        if ($this->ready) {
108
+            // read cache index
109
+            $old_index = array();
110
+            $cursor = $this->db->cache->find(array('resource' => $this->resource_uri), array('msguid' => 1, 'uid' => 1));
111
+            foreach ($cursor as $doc) {
112
+                $old_index[] = $doc['msguid'];
113
+                $this->uid2msg[$doc['uid']] = $doc['msguid'];
114
+            }
115
+
116
+            // fetch new objects from imap
117
+            foreach (array_diff($this->index, $old_index) as $msguid) {
118
+                if ($object = $this->folder->read_object($msguid, '*')) {
119
+                    try {
120
+                        $this->db->cache->insert($this->_serialize($object, $msguid));
121
+                    }
122
+                    catch (Exception $e) {
123
+                        rcmail::raise_error(array(
124
+                            'code' => 900, 'type' => 'php',
125
+                            'message' => "Failed to write to mongodb cache: " . $e->getMessage(),
126
+                        ), true);
127
+                    }
128
+                }
129
+            }
130
+
131
+            // delete invalid entries from local DB
132
+            $del_index = array_diff($old_index, $this->index);
133
+            if (!empty($del_index)) {
134
+                $this->db->cache->remove(array('resource' => $this->resource_uri, 'msguid' => array('$in' => $del_index)));
135
+            }
136
+        }
137
+
138
+        // remove lock
139
+        $this->_sync_unlock();
140
+
141
+        $this->synched = time();
142
+    }
143
+
144
+
145
+    /**
146
+     * Read a single entry from cache or from IMAP directly
147
+     *
148
+     * @param string Related IMAP message UID
149
+     * @param string Object type to read
150
+     * @param string IMAP folder name the entry relates to
151
+     * @param array  Hash array with object properties or null if not found
152
+     */
153
+    public function get($msguid, $type = null, $foldername = null)
154
+    {
155
+        // delegate to another cache instance
156
+        if ($foldername && $foldername != $this->folder->name) {
157
+            return kolab_storage::get_folder($foldername)->cache->get($msguid, $object);
158
+        }
159
+
160
+        // load object if not in memory
161
+        if (!isset($this->objects[$msguid])) {
162
+            if ($this->ready && ($doc = $this->db->cache->findOne(array('resource' => $this->resource_uri, 'msguid' => $msguid))))
163
+                $this->objects[$msguid] = $this->_unserialize($doc);
164
+
165
+            // fetch from IMAP if not present in cache
166
+            if (empty($this->objects[$msguid])) {
167
+                $result = $this->_fetch(array($msguid), $type, $foldername);
168
+                $this->objects[$msguid] = $result[0];
169
+            }
170
+        }
171
+
172
+        return $this->objects[$msguid];
173
+    }
174
+
175
+
176
+    /**
177
+     * Insert/Update a cache entry
178
+     *
179
+     * @param string Related IMAP message UID
180
+     * @param mixed  Hash array with object properties to save or false to delete the cache entry
181
+     * @param string IMAP folder name the entry relates to
182
+     */
183
+    public function set($msguid, $object, $foldername = null)
184
+    {
185
+        // delegate to another cache instance
186
+        if ($foldername && $foldername != $this->folder->name) {
187
+            kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
188
+            return;
189
+        }
190
+
191
+        // write to cache
192
+        if ($this->ready) {
193
+            // remove old entry
194
+            $this->db->cache->remove(array('resource' => $this->resource_uri, 'msguid' => $msguid));
195
+
196
+            // write new object data if not false (wich means deleted)
197
+            if ($object) {
198
+                try {
199
+                    $this->db->cache->insert($this->_serialize($object, $msguid));
200
+                }
201
+                catch (Exception $e) {
202
+                    rcmail::raise_error(array(
203
+                        'code' => 900, 'type' => 'php',
204
+                        'message' => "Failed to write to mongodb cache: " . $e->getMessage(),
205
+                    ), true);
206
+                }
207
+            }
208
+        }
209
+
210
+        // keep a copy in memory for fast access
211
+        $this->objects[$msguid] = $object;
212
+
213
+        if ($object)
214
+            $this->uid2msg[$object['uid']] = $msguid;
215
+    }
216
+
217
+    /**
218
+     * Move an existing cache entry to a new resource
219
+     *
220
+     * @param string Entry's IMAP message UID
221
+     * @param string Entry's Object UID
222
+     * @param string Target IMAP folder to move it to
223
+     */
224
+    public function move($msguid, $objuid, $target_folder)
225
+    {
226
+        $target = kolab_storage::get_folder($target_folder);
227
+
228
+        // resolve new message UID in target folder
229
+        if ($new_msguid = $target->cache->uid2msguid($objuid)) {
230
+/*
231
+            $this->db->query(
232
+                "UPDATE kolab_cache SET resource=?, msguid=? ".
233
+                "WHERE resource=? AND msguid=? AND type<>?",
234
+                $target->get_resource_uri(),
235
+                $new_msguid,
236
+                $this->resource_uri,
237
+                $msguid,
238
+                'lock'
239
+            );
240
+*/
241
+        }
242
+        else {
243
+            // just clear cache entry
244
+            $this->set($msguid, false);
245
+        }
246
+
247
+        unset($this->uid2msg[$uid]);
248
+    }
249
+
250
+
251
+    /**
252
+     * Remove all objects from local cache
253
+     */
254
+    public function purge($type = null)
255
+    {
256
+        return $this->db->cache->remove(array(), array('safe' => true));
257
+    }
258
+
259
+
260
+    /**
261
+     * Select Kolab objects filtered by the given query
262
+     *
263
+     * @param array Pseudo-SQL query as list of filter parameter triplets
264
+     *   triplet: array('<colname>', '<comparator>', '<value>')
265
+     * @return array List of Kolab data objects (each represented as hash array)
266
+     */
267
+    public function select($query = array())
268
+    {
269
+        $result = array();
270
+
271
+        // read from local cache DB (assume it to be synchronized)
272
+        if ($this->ready) {
273
+            $cursor = $this->db->cache->find(array('resource' => $this->resource_uri) + $this->_mongo_filter($query));
274
+            foreach ($cursor as $doc) {
275
+                if ($object = $this->_unserialize($doc))
276
+                    $result[] = $object;
277
+            }
278
+        }
279
+        else {
280
+            // extract object type from query parameter
281
+            $filter = $this->_query2assoc($query);
282
+
283
+            // use 'list' for folder's default objects
284
+            if ($filter['type'] == $this->type) {
285
+                $index = $this->index;
286
+            }
287
+            else {  // search by object type
288
+                $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_storage_folder::KTYPE_PREFIX . $filter['type'];
289
+                $index = $this->imap->search_once($this->folder->name, $search)->get();
290
+            }
291
+
292
+            // fetch all messages in $index from IMAP
293
+            $result = $this->_fetch($index, $filter['type']);
294
+
295
+            // TODO: post-filter result according to query
296
+        }
297
+
298
+        return $result;
299
+    }
300
+
301
+
302
+    /**
303
+     * Get number of objects mathing the given query
304
+     *
305
+     * @param array  $query Pseudo-SQL query as list of filter parameter triplets
306
+     * @return integer The number of objects of the given type
307
+     */
308
+    public function count($query = array())
309
+    {
310
+        $count = 0;
311
+
312
+        // cache is in sync, we can count records in local DB
313
+        if ($this->synched) {
314
+            $cursor = $this->db->cache->find(array('resource' => $this->resource_uri) + $this->_mongo_filter($query));
315
+            $count = $cursor->valid() ? $cursor->count() : 0;
316
+        }
317
+        else {
318
+            // search IMAP by object type
319
+            $filter = $this->_query2assoc($query);
320
+            $ctype  = kolab_storage_folder::KTYPE_PREFIX . $filter['type'];
321
+            $index = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
322
+            $count = $index->count();
323
+        }
324
+
325
+        return $count;
326
+    }
327
+
328
+    /**
329
+     * Helper method to convert the pseudo-SQL query into a valid mongodb filter
330
+     */
331
+    private function _mongo_filter($query)
332
+    {
333
+        $filters = array();
334
+        foreach ($query as $param) {
335
+            $filter = array();
336
+            if ($param[1] == '=' && is_array($param[2])) {
337
+                $filter[$param[0]] = array('$in' => $param[2]);
338
+                $filters[] = $filter;
339
+            }
340
+            else if ($param[1] == '=') {
341
+                $filters[] = array($param[0] => $param[2]);
342
+            }
343
+            else if ($param[1] == 'LIKE' || $param[1] == '~') {
344
+                $filter[$param[0]] = array('$regex' => preg_quote($param[2]), '$options' => 'i');
345
+                $filters[] = $filter;
346
+            }
347
+            else if ($param[1] == '!~' || $param[1] == '!LIKE') {
348
+                $filter[$param[0]] = array('$not' => '/' . preg_quote($param[2]) . '/i');
349
+                $filters[] = $filter;
350
+            }
351
+            else {
352
+                $op = '';
353
+                switch ($param[1]) {
354
+                    case '>':  $op = '$gt';  break;
355
+                    case '>=': $op = '$gte'; break;
356
+                    case '<':  $op = '$lt';  break;
357
+                    case '<=': $op = '$lte'; break;
358
+                    case '!=':
359
+                    case '<>': $op = '$gte'; break;
360
+                }
361
+                if ($op) {
362
+                    $filter[$param[0]] = array($op => $param[2]);
363
+                    $filters[] = $filter;
364
+                }
365
+            }
366
+        }
367
+
368
+        return array('$and' => $filters);
369
+    }
370
+
371
+    /**
372
+     * Helper method to convert the given pseudo-query triplets into
373
+     * an associative filter array with 'equals' values only
374
+     */
375
+    private function _query2assoc($query)
376
+    {
377
+        // extract object type from query parameter
378
+        $filter = array();
379
+        foreach ($query as $param) {
380
+            if ($param[1] == '=')
381
+                $filter[$param[0]] = $param[2];
382
+        }
383
+        return $filter;
384
+    }
385
+
386
+    /**
387
+     * Fetch messages from IMAP
388
+     *
389
+     * @param array List of message UIDs to fetch
390
+     * @return array List of parsed Kolab objects
391
+     */
392
+    private function _fetch($index, $type = null, $folder = null)
393
+    {
394
+        $results = array();
395
+        foreach ((array)$index as $msguid) {
396
+            if ($object = $this->folder->read_object($msguid, $type, $folder)) {
397
+                $results[] = $object;
398
+                $this->set($msguid, $object);
399
+            }
400
+        }
401
+
402
+        return $results;
403
+    }
404
+
405
+
406
+    /**
407
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
408
+     */
409
+    private function _serialize($object, $msguid)
410
+    {
411
+        $bincols = array_flip($this->binary_cols);
412
+        $doc = array(
413
+            'resource' => $this->resource_uri,
414
+            'type'     => $object['_type'] ? $object['_type'] : $this->folder->type,
415
+            'msguid'   => $msguid,
416
+            'uid'      => $object['uid'],
417
+            'xml'      => '',
418
+            'tags'     => array(),
419
+            'words'    => array(),
420
+            'objcols'  => array(),
421
+        );
422
+
423
+        // set type specific values
424
+        if ($this->folder->type == 'event') {
425
+            // database runs in server's timezone so using date() is what we want
426
+            $doc['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
427
+            $doc['dtend']   = date('Y-m-d H:i:s', is_object($object['end'])   ? $object['end']->format('U')   : $object['end']);
428
+
429
+            // extend date range for recurring events
430
+            if ($object['recurrence']) {
431
+                $doc['dtend'] = date('Y-m-d H:i:s', $object['recurrence']['UNTIL'] ?: strtotime('now + 2 years'));
432
+            }
433
+        }
434
+
435
+        if ($object['_formatobj']) {
436
+            $doc['xml'] = preg_replace('!(</?[a-z0-9:-]+>)[\n\r\t\s]+!ms', '$1', (string)$object['_formatobj']->write());
437
+            $doc['tags'] = $object['_formatobj']->get_tags();
438
+            $doc['words'] = $object['_formatobj']->get_words();
439
+        }
440
+
441
+        // extract object data
442
+        $data = array();
443
+        foreach ($object as $key => $val) {
444
+            if ($val === "" || $val === null) {
445
+                // skip empty properties
446
+                continue;
447
+            }
448
+            if (isset($bincols[$key])) {
449
+                $data[$key] = base64_encode($val);
450
+            }
451
+            else if (is_object($val)) {
452
+                if (is_a($val, 'DateTime')) {
453
+                    $data[$key] = array('_class' => 'DateTime', 'date' => $val->format('Y-m-d H:i:s'), 'timezone' => $val->getTimezone()->getName());
454
+                    $doc['objcols'][] = $key;
455
+                }
456
+            }
457
+            else if ($key[0] != '_') {
458
+                $data[$key] = $val;
459
+            }
460
+            else if ($key == '_attachments') {
461
+                foreach ($val as $k => $att) {
462
+                    unset($att['content'], $att['path']);
463
+                    if ($att['id'])
464
+                        $data[$key][$k] = $att;
465
+                }
466
+            }
467
+        }
468
+
469
+        $doc['data'] = $data;
470
+        return $doc;
471
+    }
472
+
473
+    /**
474
+     * Helper method to turn stored cache data into a valid storage object
475
+     */
476
+    private function _unserialize($doc)
477
+    {
478
+        $object = $doc['data'];
479
+
480
+        // decode binary properties
481
+        foreach ($this->binary_cols as $key) {
482
+            if (!empty($object[$key]))
483
+                $object[$key] = base64_decode($object[$key]);
484
+        }
485
+
486
+        // restore serialized objects
487
+        foreach ((array)$doc['objcols'] as $key) {
488
+            switch ($object[$key]['_class']) {
489
+                case 'DateTime':
490
+                    $val = new DateTime($object[$key]['date'], new DateTimeZone($object[$key]['timezone']));
491
+                    $object[$key] = $val;
492
+                    break;
493
+            }
494
+        }
495
+
496
+        // add meta data
497
+        $object['_type'] = $doc['type'];
498
+        $object['_msguid'] = $doc['msguid'];
499
+        $object['_mailbox'] = $this->folder->name;
500
+        $object['_formatobj'] = kolab_format::factory($doc['type'], $doc['xml']);
501
+
502
+        return $object;
503
+    }
504
+
505
+    /**
506
+     * Check lock record for this folder and wait if locked or set lock
507
+     */
508
+    private function _sync_lock()
509
+    {
510
+        if (!$this->ready)
511
+            return;
512
+
513
+        $this->synclock = true;
514
+        $lock = $this->db->locks->findOne(array('resource' => $this->resource_uri));
515
+
516
+        // create lock record if not exists
517
+        if (!$lock) {
518
+            $this->db->locks->insert(array('resource' => $this->resource_uri, 'created' => time()));
519
+        }
520
+        // wait if locked (expire locks after 10 minutes)
521
+        else if ((time() - $lock['created']) < 600) {
522
+            usleep(500000);
523
+            return $this->_sync_lock();
524
+        }
525
+        // set lock
526
+        else {
527
+            $lock['created'] = time();
528
+            $this->db->locks->update(array('_id' => $lock['_id']), $lock, array('safe' => true));
529
+        }
530
+    }
531
+
532
+    /**
533
+     * Remove lock for this folder
534
+     */
535
+    public function _sync_unlock()
536
+    {
537
+        if (!$this->ready || !$this->synclock)
538
+            return;
539
+
540
+        $this->db->locks->remove(array('resource' => $this->resource_uri));
541
+    }
542
+
543
+    /**
544
+     * Resolve an object UID into an IMAP message UID
545
+     *
546
+     * @param string  Kolab object UID
547
+     * @param boolean Include deleted objects
548
+     * @return int The resolved IMAP message UID
549
+     */
550
+    public function uid2msguid($uid, $deleted = false)
551
+    {
552
+        if (!isset($this->uid2msg[$uid])) {
553
+            // use IMAP SEARCH to get the right message
554
+            $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') . 'HEADER SUBJECT ' . $uid);
555
+            $results = $index->get();
556
+            $this->uid2msg[$uid] = $results[0];
557
+        }
558
+
559
+        return $this->uid2msg[$uid];
560
+    }
561
+
562
+}
563
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_note.php Added
30
 
1
@@ -0,0 +1,27 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab storage cache class for note objects
6
+ *
7
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
8
+ *
9
+ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
10
+ *
11
+ * This program is free software: you can redistribute it and/or modify
12
+ * it under the terms of the GNU Affero General Public License as
13
+ * published by the Free Software Foundation, either version 3 of the
14
+ * License, or (at your option) any later version.
15
+ *
16
+ * This program is distributed in the hope that it will be useful,
17
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ * GNU Affero General Public License for more details.
20
+ *
21
+ * You should have received a copy of the GNU Affero General Public License
22
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
+ */
24
+
25
+class kolab_storage_cache_note extends kolab_storage_cache
26
+{
27
+    
28
+}
29
\ No newline at end of file
30
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_task.php Added
47
 
1
@@ -0,0 +1,44 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab storage cache class for task objects
6
+ *
7
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
8
+ *
9
+ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
10
+ *
11
+ * This program is free software: you can redistribute it and/or modify
12
+ * it under the terms of the GNU Affero General Public License as
13
+ * published by the Free Software Foundation, either version 3 of the
14
+ * License, or (at your option) any later version.
15
+ *
16
+ * This program is distributed in the hope that it will be useful,
17
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ * GNU Affero General Public License for more details.
20
+ *
21
+ * You should have received a copy of the GNU Affero General Public License
22
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
+ */
24
+
25
+class kolab_storage_cache_task extends kolab_storage_cache
26
+{
27
+    protected $extra_cols = array('dtstart','dtend');
28
+
29
+    /**
30
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
31
+     *
32
+     * @override
33
+     */
34
+    protected function _serialize($object)
35
+    {
36
+        $sql_data = parent::_serialize($object) + array('dtstart' => null, 'dtend' => null);
37
+
38
+        if ($object['start'])
39
+            $sql_data['dtstart'] = is_object($object['start']) ? $object['start']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['start']);
40
+        if ($object['due'])
41
+            $sql_data['dtend']   = is_object($object['due'])   ? $object['due']->format(self::DB_DATE_FORMAT)   : date(self::DB_DATE_FORMAT, $object['due']);
42
+
43
+        return $sql_data;
44
+    }
45
+}
46
\ No newline at end of file
47
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_config.php Added
868
 
1
@@ -0,0 +1,866 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab storage class providing access to configuration objects on a Kolab server.
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ * @author Aleksander Machniak <machniak@kolabsys.com>
10
+ *
11
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
12
+ *
13
+ * This program is free software: you can redistribute it and/or modify
14
+ * it under the terms of the GNU Affero General Public License as
15
+ * published by the Free Software Foundation, either version 3 of the
16
+ * License, or (at your option) any later version.
17
+ *
18
+ * This program is distributed in the hope that it will be useful,
19
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
+ * GNU Affero General Public License for more details.
22
+ *
23
+ * You should have received a copy of the GNU Affero General Public License
24
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
25
+ */
26
+
27
+class kolab_storage_config
28
+{
29
+    const FOLDER_TYPE = 'configuration';
30
+
31
+
32
+    /**
33
+     * Singleton instace of kolab_storage_config
34
+     *
35
+     * @var kolab_storage_config
36
+     */
37
+    static protected $instance;
38
+
39
+    private $folders;
40
+    private $default;
41
+    private $enabled;
42
+
43
+
44
+    /**
45
+     * This implements the 'singleton' design pattern
46
+     *
47
+     * @return kolab_storage_config The one and only instance
48
+     */
49
+    static function get_instance()
50
+    {
51
+        if (!self::$instance) {
52
+            self::$instance = new kolab_storage_config();
53
+        }
54
+
55
+        return self::$instance;
56
+    }
57
+
58
+    /**
59
+     * Private constructor (finds default configuration folder as a config source)
60
+     */
61
+    private function __construct()
62
+    {
63
+        // get all configuration folders
64
+        $this->folders = kolab_storage::get_folders(self::FOLDER_TYPE, false);
65
+
66
+        foreach ($this->folders as $folder) {
67
+            if ($folder->default) {
68
+                $this->default = $folder;
69
+                break;
70
+            }
71
+        }
72
+
73
+        // if no folder is set as default, choose the first one
74
+        if (!$this->default) {
75
+            $this->default = reset($this->folders);
76
+        }
77
+
78
+        // attempt to create a default folder if it does not exist
79
+        if (!$this->default) {
80
+            $folder_name = 'Configuration';
81
+            $folder_type = self::FOLDER_TYPE . '.default';
82
+
83
+            if (kolab_storage::folder_create($folder_name, $folder_type, true)) {
84
+                $this->default = new kolab_storage_folder($folder_name, $folder_type);
85
+            }
86
+        }
87
+
88
+        // check if configuration folder exist
89
+        if ($this->default && $this->default->name) {
90
+            $this->enabled = true;
91
+        }
92
+    }
93
+
94
+    /**
95
+     * Check wether any configuration storage (folder) exists
96
+     *
97
+     * @return bool
98
+     */
99
+    public function is_enabled()
100
+    {
101
+        return $this->enabled;
102
+    }
103
+
104
+    /**
105
+     * Get configuration objects
106
+     *
107
+     * @param array $filter  Search filter
108
+     * @param bool  $default Enable to get objects only from default folder
109
+     * @param int   $limit   Max. number of records (per-folder)
110
+     *
111
+     * @return array List of objects
112
+     */
113
+    public function get_objects($filter = array(), $default = false, $limit = 0)
114
+    {
115
+        $list = array();
116
+
117
+        foreach ($this->folders as $folder) {
118
+            // we only want to read from default folder
119
+            if ($default && !$folder->default) {
120
+                continue;
121
+            }
122
+
123
+            // for better performance it's good to assume max. number of records
124
+            if ($limit) {
125
+                $folder->set_order_and_limit(null, $limit);
126
+            }
127
+
128
+            foreach ($folder->select($filter) as $object) {
129
+                unset($object['_formatobj']);
130
+                $list[] = $object;
131
+            }
132
+        }
133
+
134
+        return $list;
135
+    }
136
+
137
+    /**
138
+     * Get configuration object
139
+     *
140
+     * @param string $uid     Object UID
141
+     * @param bool   $default Enable to get objects only from default folder
142
+     *
143
+     * @return array Object data
144
+     */
145
+    public function get_object($uid, $default = false)
146
+    {
147
+        foreach ($this->folders as $folder) {
148
+            // we only want to read from default folder
149
+            if ($default && !$folder->default) {
150
+                continue;
151
+            }
152
+
153
+            if ($object = $folder->get_object($uid)) {
154
+                return $object;
155
+            }
156
+        }
157
+    }
158
+
159
+    /**
160
+     * Create/update configuration object
161
+     *
162
+     * @param array  $object Object data
163
+     * @param string $type   Object type
164
+     *
165
+     * @return bool True on success, False on failure
166
+     */
167
+    public function save(&$object, $type)
168
+    {
169
+        if (!$this->enabled) {
170
+            return false;
171
+        }
172
+
173
+        $folder = $this->find_folder($object);
174
+
175
+        if ($type) {
176
+            $object['type'] = $type;
177
+        }
178
+
179
+        return $folder->save($object, self::FOLDER_TYPE . '.' . $object['type'], $object['uid']);
180
+    }
181
+
182
+    /**
183
+     * Remove configuration object
184
+     *
185
+     * @param string $uid Object UID
186
+     *
187
+     * @return bool True on success, False on failure
188
+     */
189
+    public function delete($uid)
190
+    {
191
+        if (!$this->enabled) {
192
+            return false;
193
+        }
194
+
195
+        // fetch the object to find folder
196
+        $object = $this->get_object($uid);
197
+
198
+        if (!$object) {
199
+            return false;
200
+        }
201
+
202
+        $folder = $this->find_folder($object);
203
+
204
+        return $folder->delete($uid);
205
+    }
206
+
207
+    /**
208
+     * Find folder
209
+     */
210
+    public function find_folder($object = array())
211
+    {
212
+        // find folder object
213
+        if ($object['_mailbox']) {
214
+            foreach ($this->folders as $folder) {
215
+                if ($folder->name == $object['_mailbox']) {
216
+                    break;
217
+                }
218
+            }
219
+        }
220
+        else {
221
+            $folder = $this->default;
222
+        }
223
+
224
+        return $folder;
225
+    }
226
+
227
+    /**
228
+     * Builds relation member URI
229
+     *
230
+     * @param string|array Object UUID or Message folder, UID, Search headers (Message-Id, Date)
231
+     *
232
+     * @return string $url Member URI
233
+     */
234
+    public static function build_member_url($params)
235
+    {
236
+        // param is object UUID
237
+        if (is_string($params) && !empty($params)) {
238
+            return 'urn:uuid:' . $params;
239
+        }
240
+
241
+        if (empty($params) || !strlen($params['folder'])) {
242
+            return null;
243
+        }
244
+
245
+        $rcube   = rcube::get_instance();
246
+        $storage = $rcube->get_storage();
247
+
248
+        // modify folder spec. according to namespace
249
+        $folder = $params['folder'];
250
+        $ns     = $storage->folder_namespace($folder);
251
+
252
+        if ($ns == 'shared') {
253
+            // Note: this assumes there's only one shared namespace root
254
+            if ($ns = $storage->get_namespace('shared')) {
255
+                if ($prefix = $ns[0][0]) {
256
+                    $folder = 'shared' . substr($folder, strlen($prefix));
257
+                }
258
+            }
259
+        }
260
+        else {
261
+            if ($ns == 'other') {
262
+                // Note: this assumes there's only one other users namespace root
263
+                if ($ns = $storage->get_namespace('shared')) {
264
+                    if ($prefix = $ns[0][0]) {
265
+                        $folder = 'user' . substr($folder, strlen($prefix));
266
+                    }
267
+                }
268
+            }
269
+            else {
270
+                $folder = 'user' . '/' . $rcube->get_user_name() . '/' . $folder;
271
+            }
272
+        }
273
+
274
+        $folder = implode('/', array_map('rawurlencode', explode('/', $folder)));
275
+
276
+        // build URI
277
+        $url = 'imap:///' . $folder;
278
+
279
+        // UID is optional here because sometimes we want
280
+        // to build just a member uri prefix
281
+        if ($params['uid']) {
282
+            $url .= '/' . $params['uid'];
283
+        }
284
+
285
+        unset($params['folder']);
286
+        unset($params['uid']);
287
+
288
+        if (!empty($params)) {
289
+            $url .= '?' . http_build_query($params, '', '&');
290
+        }
291
+
292
+        return $url;
293
+    }
294
+
295
+    /**
296
+     * Parses relation member string
297
+     *
298
+     * @param string $url Member URI
299
+     *
300
+     * @return array Message folder, UID, Search headers (Message-Id, Date)
301
+     */
302
+    public static function parse_member_url($url)
303
+    {
304
+        // Look for IMAP URI:
305
+        // imap:///(user/username@domain|shared)/<folder>/<UID>?<search_params>
306
+        if (strpos($url, 'imap:///') === 0) {
307
+            $rcube   = rcube::get_instance();
308
+            $storage = $rcube->get_storage();
309
+
310
+            // parse_url does not work with imap:/// prefix
311
+            $url   = parse_url(substr($url, 8));
312
+            $path  = explode('/', $url['path']);
313
+            parse_str($url['query'], $params);
314
+
315
+            $uid  = array_pop($path);
316
+            $ns   = array_shift($path);
317
+            $path = array_map('rawurldecode', $path);
318
+
319
+            // resolve folder name
320
+            if ($ns == 'shared') {
321
+                $folder = implode('/', $path);
322
+                // Note: this assumes there's only one shared namespace root
323
+                if ($ns = $storage->get_namespace('shared')) {
324
+                    if ($prefix = $ns[0][0]) {
325
+                        $folder = $prefix . '/' . $folder;
326
+                    }
327
+                }
328
+            }
329
+            else if ($ns == 'user') {
330
+                $username = array_shift($path);
331
+                $folder   = implode('/', $path);
332
+
333
+                if ($username != $rcube->get_user_name()) {
334
+                    // Note: this assumes there's only one other users namespace root
335
+                    if ($ns = $storage->get_namespace('other')) {
336
+                        if ($prefix = $ns[0][0]) {
337
+                            $folder = $prefix . '/' . $username . '/' . $folder;
338
+                        }
339
+                    }
340
+                }
341
+                else if (!strlen($folder)) {
342
+                    $folder = 'INBOX';
343
+                }
344
+            }
345
+            else {
346
+                return;
347
+            }
348
+
349
+            return array(
350
+                'folder' => $folder,
351
+                'uid'    => $uid,
352
+                'params' => $params,
353
+            );
354
+        }
355
+
356
+        return false;
357
+    }
358
+
359
+    /**
360
+     * Build array of member URIs from set of messages
361
+     *
362
+     * @param string $folder   Folder name
363
+     * @param array  $messages Array of rcube_message objects
364
+     *
365
+     * @return array List of members (IMAP URIs)
366
+     */
367
+    public static function build_members($folder, $messages)
368
+    {
369
+        $members = array();
370
+
371
+        foreach ((array) $messages as $msg) {
372
+            $params = array(
373
+                'folder' => $folder,
374
+                'uid'    => $msg->uid,
375
+            );
376
+
377
+            // add search parameters:
378
+            // we don't want to build "invalid" searches e.g. that
379
+            // will return false positives (more or wrong messages)
380
+            if (($messageid = $msg->get('message-id', false)) && ($date = $msg->get('date', false))) {
381
+                $params['message-id'] = $messageid;
382
+                $params['date']       = $date;
383
+
384
+                if ($subject = $msg->get('subject', false)) {
385
+                    $params['subject'] = substr($subject, 0, 256);
386
+                }
387
+            }
388
+
389
+            $members[] = self::build_member_url($params);
390
+        }
391
+
392
+        return $members;
393
+    }
394
+
395
+    /**
396
+     * Resolve/validate/update members (which are IMAP URIs) of relation object.
397
+     *
398
+     * @param array $tag   Tag object
399
+     * @param bool  $force Force members list update
400
+     *
401
+     * @return array Folder/UIDs list
402
+     */
403
+    public static function resolve_members(&$tag, $force = true)
404
+    {
405
+        $result = array();
406
+
407
+        foreach ((array) $tag['members'] as $member) {
408
+            // IMAP URI members
409
+            if ($url = self::parse_member_url($member)) {
410
+                $folder = $url['folder'];
411
+
412
+                if (!$force) {
413
+                    $result[$folder][] = $url['uid'];
414
+                }
415
+                else {
416
+                    $result[$folder]['uid'][]    = $url['uid'];
417
+                    $result[$folder]['params'][] = $url['params'];
418
+                    $result[$folder]['member'][] = $member;
419
+                }
420
+            }
421
+        }
422
+
423
+        if (empty($result) || !$force) {
424
+            return $result;
425
+        }
426
+
427
+        $rcube   = rcube::get_instance();
428
+        $storage = $rcube->get_storage();
429
+        $search  = array();
430
+        $missing = array();
431
+
432
+        // first we search messages by Folder+UID
433
+        foreach ($result as $folder => $data) {
434
+            // @FIXME: maybe better use index() which is cached?
435
+            // @TODO: consider skip_deleted option
436
+            $index = $storage->search_once($folder, 'UID ' . rcube_imap_generic::compressMessageSet($data['uid']));
437
+            $uids  = $index->get();
438
+
439
+            // messages that were not found need to be searched by search parameters
440
+            $not_found = array_diff($data['uid'], $uids);
441
+            if (!empty($not_found)) {
442
+                foreach ($not_found as $uid) {
443
+                    $idx = array_search($uid, $data['uid']);
444
+
445
+                    if ($p = $data['params'][$idx]) {
446
+                        $search[] = $p;
447
+                    }
448
+
449
+                    $missing[] = $result[$folder]['member'][$idx];
450
+
451
+                    unset($result[$folder]['uid'][$idx]);
452
+                    unset($result[$folder]['params'][$idx]);
453
+                    unset($result[$folder]['member'][$idx]);
454
+                }
455
+            }
456
+
457
+            $result[$folder] = $uids;
458
+        }
459
+
460
+        // search in all subscribed mail folders using search parameters
461
+        if (!empty($search)) {
462
+            // remove not found members from the members list
463
+            $tag['members'] = array_diff($tag['members'], $missing);
464
+
465
+            // get subscribed folders
466
+            $folders = $storage->list_folders_subscribed('', '*', 'mail', null, true);
467
+
468
+            // @TODO: do this search in chunks (for e.g. 10 messages)?
469
+            $search_str = '';
470
+
471
+            foreach ($search as $p) {
472
+                $search_params = array();
473
+                foreach ($p as $key => $val) {
474
+                    $key = strtoupper($key);
475
+                    // don't search by subject, we don't want false-positives
476
+                    if ($key != 'SUBJECT') {
477
+                        $search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val);
478
+                    }
479
+                }
480
+
481
+                $search_str .= ' (' . implode(' ', $search_params) . ')';
482
+            }
483
+
484
+            $search_str = trim(str_repeat(' OR', count($search)-1) . $search_str);
485
+
486
+            // search
487
+            $search = $storage->search_once($folders, $search_str);
488
+
489
+            // handle search result
490
+            $folders = (array) $search->get_parameters('MAILBOX');
491
+
492
+            foreach ($folders as $folder) {
493
+                $set  = $search->get_set($folder);
494
+                $uids = $set->get();
495
+
496
+                if (!empty($uids)) {
497
+                    $msgs    = $storage->fetch_headers($folder, $uids, false);
498
+                    $members = self::build_members($folder, $msgs);
499
+
500
+                    // merge new members into the tag members list
501
+                    $tag['members'] = array_merge($tag['members'], $members);
502
+
503
+                    // add UIDs into the result
504
+                    $result[$folder] = array_unique(array_merge((array)$result[$folder], $uids));
505
+                }
506
+            }
507
+
508
+            // update tag object with new members list
509
+            $tag['members'] = array_unique($tag['members']);
510
+            kolab_storage_config::get_instance()->save($tag, 'relation', false);
511
+        }
512
+
513
+        return $result;
514
+    }
515
+
516
+    /**
517
+     * Assign tags to kolab objects
518
+     *
519
+     * @param array $records List of kolab objects
520
+     *
521
+     * @return array List of tags
522
+     */
523
+    public function apply_tags(&$records)
524
+    {
525
+        // first convert categories into tags
526
+        foreach ($records as $i => $rec) {
527
+            if (!empty($rec['categories'])) {
528
+                $folder = new kolab_storage_folder($rec['_mailbox']);
529
+                if ($object = $folder->get_object($rec['uid'])) {
530
+                    $tags = $rec['categories'];
531
+
532
+                    unset($object['categories']);
533
+                    unset($records[$i]['categories']);
534
+
535
+                    $this->save_tags($rec['uid'], $tags);
536
+                    $folder->save($object, $rec['_type'], $rec['uid']);
537
+                }
538
+            }
539
+        }
540
+
541
+        $tags = array();
542
+
543
+        // assign tags to objects
544
+        foreach ($this->get_tags() as $tag) {
545
+            foreach ($records as $idx => $rec) {
546
+                $uid = self::build_member_url($rec['uid']);
547
+                if (in_array($uid, (array) $tag['members'])) {
548
+                    $records[$idx]['tags'][] = $tag['name'];
549
+                }
550
+            }
551
+
552
+            $tags[] = $tag['name'];
553
+        }
554
+
555
+        $tags = array_unique($tags);
556
+
557
+        return $tags;
558
+    }
559
+
560
+    /**
561
+     * Update object tags
562
+     *
563
+     * @param string $uid  Kolab object UID
564
+     * @param array  $tags List of tag names
565
+     */
566
+    public function save_tags($uid, $tags)
567
+    {
568
+        $url       = self::build_member_url($uid);
569
+        $relations = $this->get_tags();
570
+
571
+        foreach ($relations as $idx => $relation) {
572
+            $selected = !empty($tags) && in_array($relation['name'], $tags);
573
+            $found    = !empty($relation['members']) && in_array($url, $relation['members']);
574
+            $update   = false;
575
+
576
+            // remove member from the relation
577
+            if ($found && !$selected) {
578
+                $relation['members'] = array_diff($relation['members'], (array) $url);
579
+                $update = true;
580
+            }
581
+            // add member to the relation
582
+            else if (!$found && $selected) {
583
+                $relation['members'][] = $url;
584
+                $update = true;
585
+            }
586
+
587
+            if ($update) {
588
+                if ($this->save($relation, 'relation')) {
589
+                    $this->tags[$idx] = $relation; // update in-memory cache
590
+                }
591
+            }
592
+
593
+            if ($selected) {
594
+                $tags = array_diff($tags, (array)$relation['name']);
595
+            }
596
+        }
597
+
598
+        // create new relations
599
+        if (!empty($tags)) {
600
+            foreach ($tags as $tag) {
601
+                $relation = array(
602
+                    'name'     => $tag,
603
+                    'members'  => (array) $url,
604
+                    'category' => 'tag',
605
+                );
606
+
607
+                if ($this->save($relation, 'relation')) {
608
+                    $this->tags[] = $relation; // update in-memory cache
609
+                }
610
+            }
611
+        }
612
+    }
613
+
614
+    /**
615
+     * Get tags (all or referring to specified object)
616
+     *
617
+     * @param string $uid Optional object UID
618
+     *
619
+     * @return array List of Relation objects
620
+     */
621
+    public function get_tags($uid = '*')
622
+    {
623
+        if (!isset($this->tags)) {
624
+            $default = true;
625
+            $filter  = array(
626
+                array('type', '=', 'relation'),
627
+                array('category', '=', 'tag')
628
+            );
629
+
630
+            // use faster method
631
+            if ($uid && $uid != '*') {
632
+                $filter[] = array('member', '=', $uid);
633
+                $tags = $this->get_objects($filter, $default);
634
+            }
635
+            else {
636
+                $this->tags = $tags = $this->get_objects($filter, $default);
637
+            }
638
+        }
639
+        else {
640
+            $tags = $this->tags;
641
+        }
642
+
643
+        if ($uid === '*') {
644
+            return $tags;
645
+        }
646
+
647
+        $result = array();
648
+        $search = self::build_member_url($uid);
649
+
650
+        foreach ($tags as $tag) {
651
+            if (in_array($search, (array) $tag['members'])) {
652
+                $result[] = $tag;
653
+            }
654
+        }
655
+
656
+        return $result;
657
+    }
658
+
659
+    /**
660
+     * Find objects linked with the given groupware object through a relation
661
+     *
662
+     * @param string Object UUID
663
+     * @param array List of related URIs
664
+     */
665
+    public function get_object_links($uid)
666
+    {
667
+        $links = array();
668
+        $object_uri = self::build_member_url($uid);
669
+
670
+        foreach ($this->get_relations_for_member($uid) as $relation) {
671
+            if (in_array($object_uri, (array) $relation['members'])) {
672
+                // make relation members up-to-date
673
+                kolab_storage_config::resolve_members($relation);
674
+
675
+                foreach ($relation['members'] as $member) {
676
+                    if ($member != $object_uri) {
677
+                        $links[] = $member;
678
+                    }
679
+                }
680
+            }
681
+        }
682
+
683
+        return array_unique($links);
684
+    }
685
+
686
+    /**
687
+     *
688
+     */
689
+    public function save_object_links($uid, $links, $remove = array())
690
+    {
691
+        $object_uri = self::build_member_url($uid);
692
+        $relations = $this->get_relations_for_member($uid);
693
+        $done = false;
694
+
695
+        foreach ($relations as $relation) {
696
+            // make relation members up-to-date
697
+            kolab_storage_config::resolve_members($relation);
698
+
699
+            // remove and add links
700
+            $members = array_diff($relation['members'], (array)$remove);
701
+            $members = array_unique(array_merge($members, $links));
702
+
703
+            // make sure the object_uri is still a member
704
+            if (!in_array($object_uri, $members)) {
705
+                $members[$object_uri];
706
+            }
707
+
708
+            // remove relation if no other members remain
709
+            if (count($members) <= 1) {
710
+                $done = $this->delete($relation['uid']);
711
+            }
712
+            // update relation object if members changed
713
+            else if (count(array_diff($members, $relation['members'])) || count(array_diff($relation['members'], $members))) {
714
+                $relation['members'] = $members;
715
+                $done = $this->save($relation, 'relation');
716
+                $links = array();
717
+            }
718
+            // no changes, we're happy
719
+            else {
720
+                $done = true;
721
+                $links = array();
722
+            }
723
+        }
724
+
725
+        // create a new relation
726
+        if (!$done && !empty($links)) {
727
+            $relation = array(
728
+                'members'  => array_merge($links, array($object_uri)),
729
+                'category' => 'generic',
730
+            );
731
+
732
+            $ret = $this->save($relation, 'relation');
733
+        }
734
+
735
+        return $ret;
736
+    }
737
+
738
+    /**
739
+     * Find relation objects referring to specified note
740
+     */
741
+    public function get_relations_for_member($uid, $reltype = 'generic')
742
+    {
743
+        $default = true;
744
+        $filter  = array(
745
+            array('type', '=', 'relation'),
746
+            array('category', '=', $reltype),
747
+            array('member', '=', $uid),
748
+        );
749
+
750
+        return $this->get_objects($filter, $default, 100);
751
+    }
752
+
753
+    /**
754
+     * Find kolab objects assigned to specified e-mail message
755
+     *
756
+     * @param rcube_message $message E-mail message
757
+     * @param string        $folder  Folder name
758
+     * @param string        $type    Result objects type
759
+     *
760
+     * @return array List of kolab objects
761
+     */
762
+    public function get_message_relations($message, $folder, $type)
763
+    {
764
+        static $_cache = array();
765
+
766
+        $result  = array();
767
+        $uids    = array();
768
+        $default = true;
769
+        $uri     = self::get_message_uri($message, $folder);
770
+        $filter  = array(
771
+            array('type', '=', 'relation'),
772
+            array('category', '=', 'generic'),
773
+        );
774
+
775
+        // query by message-id
776
+        $member_id = $message->get('message-id', false);
777
+        if (empty($member_id)) {
778
+            // derive message identifier from URI
779
+            $member_id = md5($uri);
780
+        }
781
+        $filter[] = array('member', '=', $member_id);
782
+
783
+        if (!isset($_cache[$uri])) {
784
+            // get UIDs of related groupware objects
785
+            foreach ($this->get_objects($filter, $default) as $relation) {
786
+                // we don't need to update members if the URI is found
787
+                if (!in_array($uri, $relation['members'])) {
788
+                    // update members...
789
+                    $messages = kolab_storage_config::resolve_members($relation);
790
+                    // ...and check again
791
+                    if (empty($messages[$folder]) || !in_array($message->uid, $messages[$folder])) {
792
+                        continue;
793
+                    }
794
+                }
795
+
796
+                // find groupware object UID(s)
797
+                foreach ($relation['members'] as $member) {
798
+                    if (strpos($member, 'urn:uuid:') === 0) {
799
+                        $uids[] = substr($member, 9);
800
+                    }
801
+                }
802
+            }
803
+
804
+            // remember this lookup
805
+            $_cache[$uri] = $uids;
806
+        }
807
+        else {
808
+            $uids = $_cache[$uri];
809
+        }
810
+
811
+        // get kolab objects of specified type
812
+        if (!empty($uids)) {
813
+            $query  = array(array('uid', '=', array_unique($uids)));
814
+            $result = kolab_storage::select($query, $type);
815
+        }
816
+
817
+        return $result;
818
+    }
819
+
820
+    /**
821
+     * Build a URI representing the given message reference
822
+     */
823
+    public static function get_message_uri($headers, $folder)
824
+    {
825
+        $params = array(
826
+            'folder' => $headers->folder ?: $folder,
827
+            'uid'    => $headers->uid,
828
+        );
829
+
830
+        if (($messageid = $headers->get('message-id', false)) && ($date = $headers->get('date', false))) {
831
+            $params['message-id'] = $messageid;
832
+            $params['date']       = $date;
833
+
834
+            if ($subject = $headers->get('subject')) {
835
+                $params['subject'] = $subject;
836
+            }
837
+        }
838
+
839
+        return self::build_member_url($params);
840
+    }
841
+
842
+    /**
843
+     * Resolve the email message reference from the given URI
844
+     */
845
+    public function get_message_reference($uri, $rel = null)
846
+    {
847
+        if ($linkref = self::parse_member_url($uri)) {
848
+            $linkref['subject'] = $linkref['params']['subject'];
849
+            $linkref['uri']     = $uri;
850
+
851
+            $rcmail = rcube::get_instance();
852
+            if (method_exists($rcmail, 'url')) {
853
+                $linkref['mailurl'] = $rcmail->url(array(
854
+                    'task'   => 'mail',
855
+                    'action' => 'show',
856
+                    'mbox'   => $linkref['folder'],
857
+                    'uid'    => $linkref['uid'],
858
+                    'rel'    => $rel,
859
+                ));
860
+            }
861
+
862
+            unset($linkref['params']);
863
+        }
864
+
865
+        return $linkref;
866
+    }
867
+}
868
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_dataset.php Added
156
 
1
@@ -0,0 +1,154 @@
2
+<?php
3
+
4
+/**
5
+ * Dataset class providing the results of a select operation on a kolab_storage_folder.
6
+ *
7
+ * Can be used as a normal array as well as an iterator in foreach() loops.
8
+ *
9
+ * @version @package_version@
10
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
11
+ *
12
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
13
+ *
14
+ * This program is free software: you can redistribute it and/or modify
15
+ * it under the terms of the GNU Affero General Public License as
16
+ * published by the Free Software Foundation, either version 3 of the
17
+ * License, or (at your option) any later version.
18
+ *
19
+ * This program is distributed in the hope that it will be useful,
20
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
+ * GNU Affero General Public License for more details.
23
+ *
24
+ * You should have received a copy of the GNU Affero General Public License
25
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
26
+ */
27
+
28
+class kolab_storage_dataset implements Iterator, ArrayAccess, Countable
29
+{
30
+    private $cache;  // kolab_storage_cache instance to use for fetching data
31
+    private $memlimit = 0;
32
+    private $buffer = false;
33
+    private $index = array();
34
+    private $data = array();
35
+    private $iteratorkey = 0;
36
+    private $error = null;
37
+
38
+    /**
39
+     * Default constructor
40
+     *
41
+     * @param object kolab_storage_cache instance to be used for fetching objects upon access
42
+     */
43
+    public function __construct($cache)
44
+    {
45
+        $this->cache = $cache;
46
+
47
+        // enable in-memory buffering up until 1/5 of the available memory
48
+        if (function_exists('memory_get_usage')) {
49
+            $this->memlimit = parse_bytes(ini_get('memory_limit')) / 5;
50
+            $this->buffer = true;
51
+        }
52
+    }
53
+
54
+    /**
55
+     * Return error state
56
+     */
57
+    public function is_error()
58
+    {
59
+        return !empty($this->error);
60
+    }
61
+
62
+    /**
63
+     * Set error state
64
+     */
65
+    public function set_error($err)
66
+    {
67
+        $this->error = $err;
68
+    }
69
+
70
+
71
+    /*** Implement PHP Countable interface ***/
72
+
73
+    public function count()
74
+    {
75
+        return count($this->index);
76
+    }
77
+
78
+
79
+    /*** Implement PHP ArrayAccess interface ***/
80
+
81
+    public function offsetSet($offset, $value)
82
+    {
83
+        $uid = $value['_msguid'];
84
+
85
+        if (is_null($offset)) {
86
+            $offset = count($this->index);
87
+            $this->index[] = $uid;
88
+        }
89
+        else {
90
+            $this->index[$offset] = $uid;
91
+        }
92
+
93
+        // keep full payload data in memory if possible
94
+        if ($this->memlimit && $this->buffer && isset($value['_mailbox'])) {
95
+            $this->data[$offset] = $value;
96
+
97
+            // check memory usage and stop buffering
98
+            if ($offset % 10 == 0) {
99
+                $this->buffer = memory_get_usage() < $this->memlimit;
100
+            }
101
+        }
102
+    }
103
+
104
+    public function offsetExists($offset)
105
+    {
106
+        return isset($this->index[$offset]);
107
+    }
108
+
109
+    public function offsetUnset($offset)
110
+    {
111
+        unset($this->index[$offset]);
112
+    }
113
+
114
+    public function offsetGet($offset)
115
+    {
116
+        if (isset($this->data[$offset])) {
117
+            return $this->data[$offset];
118
+        }
119
+        else if ($msguid = $this->index[$offset]) {
120
+            return $this->cache->get($msguid);
121
+        }
122
+
123
+        return null;
124
+    }
125
+
126
+
127
+    /*** Implement PHP Iterator interface ***/
128
+
129
+    public function current()
130
+    {
131
+        return $this->offsetGet($this->iteratorkey);
132
+    }
133
+
134
+    public function key()
135
+    {
136
+        return $this->iteratorkey;
137
+    }
138
+
139
+    public function next()
140
+    {
141
+        $this->iteratorkey++;
142
+        return $this->valid();
143
+    }
144
+
145
+    public function rewind()
146
+    {
147
+        $this->iteratorkey = 0;
148
+    }
149
+
150
+    public function valid()
151
+    {
152
+        return !empty($this->index[$this->iteratorkey]);
153
+    }
154
+
155
+}
156
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder.php Added
1168
 
1
@@ -0,0 +1,1166 @@
2
+<?php
3
+
4
+/**
5
+ * The kolab_storage_folder class represents an IMAP folder on the Kolab server.
6
+ *
7
+ * @version @package_version@
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ * @author Aleksander Machniak <machniak@kolabsys.com>
10
+ *
11
+ * Copyright (C) 2012-2013, Kolab Systems AG <contact@kolabsys.com>
12
+ *
13
+ * This program is free software: you can redistribute it and/or modify
14
+ * it under the terms of the GNU Affero General Public License as
15
+ * published by the Free Software Foundation, either version 3 of the
16
+ * License, or (at your option) any later version.
17
+ *
18
+ * This program is distributed in the hope that it will be useful,
19
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
+ * GNU Affero General Public License for more details.
22
+ *
23
+ * You should have received a copy of the GNU Affero General Public License
24
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
25
+ */
26
+class kolab_storage_folder extends kolab_storage_folder_api
27
+{
28
+    /**
29
+     * The kolab_storage_cache instance for caching operations
30
+     * @var object
31
+     */
32
+    public $cache;
33
+
34
+    /**
35
+     * Indicate validity status
36
+     * @var boolean
37
+     */
38
+    public $valid = false;
39
+
40
+    protected $error = 0;
41
+
42
+    protected $resource_uri;
43
+
44
+
45
+    /**
46
+     * Default constructor
47
+     *
48
+     * @param string The folder name/path
49
+     * @param string Expected folder type
50
+     */
51
+    function __construct($name, $type = null, $type_annotation = null)
52
+    {
53
+        parent::__construct($name);
54
+        $this->imap->set_options(array('skip_deleted' => true));
55
+        $this->set_folder($name, $type, $type_annotation);
56
+    }
57
+
58
+
59
+    /**
60
+     * Set the IMAP folder this instance connects to
61
+     *
62
+     * @param string The folder name/path
63
+     * @param string Expected folder type
64
+     * @param string Optional folder type if known
65
+     */
66
+    public function set_folder($name, $type = null, $type_annotation = null)
67
+    {
68
+        if (empty($type_annotation)) {
69
+            $type_annotation = kolab_storage::folder_type($name);
70
+        }
71
+
72
+        $oldtype = $this->type;
73
+        list($this->type, $suffix) = explode('.', $type_annotation);
74
+        $this->default      = $suffix == 'default';
75
+        $this->subtype      = $this->default ? '' : $suffix;
76
+        $this->name         = $name;
77
+        $this->id           = kolab_storage::folder_id($name);
78
+        $this->valid        = !empty($this->type) && $this->type != 'mail' && (!$type || $this->type == $type);
79
+
80
+        if (!$this->valid) {
81
+            $this->error = $this->imap->get_error_code() < 0 ? kolab_storage::ERROR_IMAP_CONN : kolab_storage::ERROR_INVALID_FOLDER;
82
+        }
83
+
84
+        // reset cached object properties
85
+        $this->owner = $this->namespace = $this->resource_uri = $this->info = $this->idata = null;
86
+
87
+        // get a new cache instance if folder type changed
88
+        if (!$this->cache || $this->type != $oldtype)
89
+            $this->cache = kolab_storage_cache::factory($this);
90
+        else
91
+            $this->cache->set_folder($this);
92
+
93
+        $this->imap->set_folder($this->name);
94
+    }
95
+
96
+    /**
97
+     * Returns code of last error
98
+     *
99
+     * @return int Error code
100
+     */
101
+    public function get_error()
102
+    {
103
+        return $this->error ?: $this->cache->get_error();
104
+    }
105
+
106
+    /**
107
+     * Check IMAP connection error state
108
+     */
109
+    public function check_error()
110
+    {
111
+        if (($err_code = $this->imap->get_error_code()) < 0) {
112
+            $this->error = kolab_storage::ERROR_IMAP_CONN;
113
+            if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) {
114
+                $this->error = kolab_storage::ERROR_NO_PERMISSION;
115
+            }
116
+        }
117
+
118
+        return $this->error;
119
+    }
120
+
121
+    /**
122
+     * Compose a unique resource URI for this IMAP folder
123
+     */
124
+    public function get_resource_uri()
125
+    {
126
+        if (!empty($this->resource_uri))
127
+            return $this->resource_uri;
128
+
129
+        // strip namespace prefix from folder name
130
+        $ns = $this->get_namespace();
131
+        $nsdata = $this->imap->get_namespace($ns);
132
+        if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) {
133
+            $subpath = substr($this->name, strlen($nsdata[0][0]));
134
+            if ($ns == 'other') {
135
+                list($user, $suffix) = explode($nsdata[0][1], $subpath, 2);
136
+                $subpath = $suffix;
137
+            }
138
+        }
139
+        else {
140
+            $subpath = $this->name;
141
+        }
142
+
143
+        // compose fully qualified ressource uri for this instance
144
+        $this->resource_uri = 'imap://' . urlencode($this->get_owner(true)) . '@' . $this->imap->options['host'] . '/' . $subpath;
145
+        return $this->resource_uri;
146
+    }
147
+
148
+    /**
149
+     * Helper method to extract folder UID metadata
150
+     *
151
+     * @return string Folder's UID
152
+     */
153
+    public function get_uid()
154
+    {
155
+        // UID is defined in folder METADATA
156
+        $metakeys = array(kolab_storage::UID_KEY_SHARED, kolab_storage::UID_KEY_PRIVATE, kolab_storage::UID_KEY_CYRUS);
157
+        $metadata = $this->get_metadata($metakeys);
158
+        foreach ($metakeys as $key) {
159
+            if (($uid = $metadata[$key])) {
160
+                return $uid;
161
+            }
162
+        }
163
+
164
+        // generate a folder UID and set it to IMAP
165
+        $uid = rtrim(chunk_split(md5($this->name . $this->get_owner() . uniqid('-', true)), 12, '-'), '-');
166
+        if ($this->set_uid($uid)) {
167
+            return $uid;
168
+        }
169
+
170
+        // create hash from folder name if we can't write the UID metadata
171
+        return md5($this->name . $this->get_owner());
172
+    }
173
+
174
+    /**
175
+     * Helper method to set an UID value to the given IMAP folder instance
176
+     *
177
+     * @param string Folder's UID
178
+     * @return boolean True on succes, False on failure
179
+     */
180
+    public function set_uid($uid)
181
+    {
182
+        if (!($success = $this->set_metadata(array(kolab_storage::UID_KEY_SHARED => $uid)))) {
183
+            $success = $this->set_metadata(array(kolab_storage::UID_KEY_PRIVATE => $uid));
184
+        }
185
+
186
+        $this->check_error();
187
+        return $success;
188
+    }
189
+
190
+    /**
191
+     * Compose a folder Etag identifier
192
+     */
193
+    public function get_ctag()
194
+    {
195
+        $fdata = $this->get_imap_data();
196
+        $this->check_error();
197
+        return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']);
198
+    }
199
+
200
+    /**
201
+     * Check activation status of this folder
202
+     *
203
+     * @return boolean True if enabled, false if not
204
+     */
205
+    public function is_active()
206
+    {
207
+        return kolab_storage::folder_is_active($this->name);
208
+    }
209
+
210
+    /**
211
+     * Change activation status of this folder
212
+     *
213
+     * @param boolean The desired subscription status: true = active, false = not active
214
+     *
215
+     * @return True on success, false on error
216
+     */
217
+    public function activate($active)
218
+    {
219
+        return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
220
+    }
221
+
222
+    /**
223
+     * Check subscription status of this folder
224
+     *
225
+     * @return boolean True if subscribed, false if not
226
+     */
227
+    public function is_subscribed()
228
+    {
229
+        return kolab_storage::folder_is_subscribed($this->name);
230
+    }
231
+
232
+    /**
233
+     * Change subscription status of this folder
234
+     *
235
+     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
236
+     *
237
+     * @return True on success, false on error
238
+     */
239
+    public function subscribe($subscribed)
240
+    {
241
+        return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
242
+    }
243
+
244
+    /**
245
+     * Get number of objects stored in this folder
246
+     *
247
+     * @param mixed  Pseudo-SQL query as list of filter parameter triplets
248
+     *    or string with object type (e.g. contact, event, todo, journal, note, configuration)
249
+     * @return integer The number of objects of the given type
250
+     * @see self::select()
251
+     */
252
+    public function count($query = null)
253
+    {
254
+        if (!$this->valid) {
255
+            return 0;
256
+        }
257
+
258
+        // synchronize cache first
259
+        $this->cache->synchronize();
260
+
261
+        return $this->cache->count($this->_prepare_query($query));
262
+    }
263
+
264
+
265
+    /**
266
+     * List all Kolab objects of the given type
267
+     *
268
+     * @param string  $type Object type (e.g. contact, event, todo, journal, note, configuration)
269
+     * @return array  List of Kolab data objects (each represented as hash array)
270
+     */
271
+    public function get_objects($type = null)
272
+    {
273
+        if (!$type) $type = $this->type;
274
+
275
+        if (!$this->valid) {
276
+            return array();
277
+        }
278
+
279
+        // synchronize caches
280
+        $this->cache->synchronize();
281
+
282
+        // fetch objects from cache
283
+        return $this->cache->select($this->_prepare_query($type));
284
+    }
285
+
286
+
287
+    /**
288
+     * Select *some* Kolab objects matching the given query
289
+     *
290
+     * @param array Pseudo-SQL query as list of filter parameter triplets
291
+     *   triplet: array('<colname>', '<comparator>', '<value>')
292
+     * @return array List of Kolab data objects (each represented as hash array)
293
+     */
294
+    public function select($query = array())
295
+    {
296
+        if (!$this->valid) {
297
+            return array();
298
+        }
299
+
300
+        // check query argument
301
+        if (empty($query)) {
302
+            return $this->get_objects();
303
+        }
304
+
305
+        // synchronize caches
306
+        $this->cache->synchronize();
307
+
308
+        // fetch objects from cache
309
+        return $this->cache->select($this->_prepare_query($query));
310
+    }
311
+
312
+
313
+    /**
314
+     * Getter for object UIDs only
315
+     *
316
+     * @param array Pseudo-SQL query as list of filter parameter triplets
317
+     * @return array List of Kolab object UIDs
318
+     */
319
+    public function get_uids($query = array())
320
+    {
321
+        if (!$this->valid) {
322
+            return array();
323
+        }
324
+
325
+        // synchronize caches
326
+        $this->cache->synchronize();
327
+
328
+        // fetch UIDs from cache
329
+        return $this->cache->select($this->_prepare_query($query), true);
330
+    }
331
+
332
+    /**
333
+     * Setter for ORDER BY and LIMIT parameters for cache queries
334
+     *
335
+     * @param array   List of columns to order by
336
+     * @param integer Limit result set to this length
337
+     * @param integer Offset row
338
+     */
339
+    public function set_order_and_limit($sortcols, $length = null, $offset = 0)
340
+    {
341
+        $this->cache->set_order_by($sortcols);
342
+
343
+        if ($length !== null) {
344
+            $this->cache->set_limit($length, $offset);
345
+        }
346
+    }
347
+
348
+    /**
349
+     * Helper method to sanitize query arguments
350
+     */
351
+    private function _prepare_query($query)
352
+    {
353
+        // string equals type query
354
+        // FIXME: should not be called this way!
355
+        if (is_string($query)) {
356
+            return $this->cache->has_type_col() && !empty($query) ? array(array('type','=',$query)) : array();
357
+        }
358
+
359
+        foreach ((array)$query as $i => $param) {
360
+            if ($param[0] == 'type' && !$this->cache->has_type_col()) {
361
+                unset($query[$i]);
362
+            }
363
+            else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) {
364
+                if (is_object($param[2]) && is_a($param[2], 'DateTime'))
365
+                    $param[2] = $param[2]->format('U');
366
+                if (is_numeric($param[2]))
367
+                    $query[$i][2] = date('Y-m-d H:i:s', $param[2]);
368
+            }
369
+        }
370
+
371
+        return $query;
372
+    }
373
+
374
+
375
+    /**
376
+     * Getter for a single Kolab object, identified by its UID
377
+     *
378
+     * @param string $uid  Object UID
379
+     * @param string $type Object type (e.g. contact, event, todo, journal, note, configuration)
380
+     *                     Defaults to folder type
381
+     *
382
+     * @return array The Kolab object represented as hash array
383
+     */
384
+    public function get_object($uid, $type = null)
385
+    {
386
+        if (!$this->valid) {
387
+            return false;
388
+        }
389
+
390
+        // synchronize caches
391
+        $this->cache->synchronize();
392
+
393
+        $msguid = $this->cache->uid2msguid($uid);
394
+
395
+        if ($msguid && ($object = $this->cache->get($msguid, $type))) {
396
+            return $object;
397
+        }
398
+
399
+        return false;
400
+    }
401
+
402
+
403
+    /**
404
+     * Fetch a Kolab object attachment which is stored in a separate part
405
+     * of the mail MIME message that represents the Kolab record.
406
+     *
407
+     * @param string   Object's UID
408
+     * @param string   The attachment's mime number
409
+     * @param string   IMAP folder where message is stored;
410
+     *                 If set, that also implies that the given UID is an IMAP UID
411
+     * @param bool     True to print the part content
412
+     * @param resource File pointer to save the message part
413
+     * @param boolean  Disables charset conversion
414
+     *
415
+     * @return mixed  The attachment content as binary string
416
+     */
417
+    public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false)
418
+    {
419
+        if ($this->valid && ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid)))) {
420
+            $this->imap->set_folder($mailbox ? $mailbox : $this->name);
421
+
422
+            if (substr($part, 0, 2) == 'i:') {
423
+                // attachment data is stored in XML
424
+                if ($object = $this->cache->get($msguid)) {
425
+                    // load data from XML (attachment content is not stored in cache)
426
+                    if ($object['_formatobj'] && isset($object['_size'])) {
427
+                        $object['_attachments'] = array();
428
+                        $object['_formatobj']->get_attachments($object);
429
+                    }
430
+
431
+                    foreach ($object['_attachments'] as $attach) {
432
+                        if ($attach['id'] == $part) {
433
+                            if ($print)   echo $attach['content'];
434
+                            else if ($fp) fwrite($fp, $attach['content']);
435
+                            else          return $attach['content'];
436
+                            return true;
437
+                        }
438
+                    }
439
+                }
440
+            }
441
+            else {
442
+                // return message part from IMAP directly
443
+                return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
444
+            }
445
+        }
446
+
447
+        return null;
448
+    }
449
+
450
+
451
+    /**
452
+     * Fetch the mime message from the storage server and extract
453
+     * the Kolab groupware object from it
454
+     *
455
+     * @param string The IMAP message UID to fetch
456
+     * @param string The object type expected (use wildcard '*' to accept all types)
457
+     * @param string The folder name where the message is stored
458
+     *
459
+     * @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
460
+     */
461
+    public function read_object($msguid, $type = null, $folder = null)
462
+    {
463
+        if (!$this->valid) {
464
+            return false;
465
+        }
466
+
467
+        if (!$type) $type = $this->type;
468
+        if (!$folder) $folder = $this->name;
469
+
470
+        $this->imap->set_folder($folder);
471
+
472
+        $this->cache->bypass(true);
473
+        $message = new rcube_message($msguid);
474
+        $this->cache->bypass(false);
475
+
476
+        // Message doesn't exist?
477
+        if (empty($message->headers)) {
478
+            return false;
479
+        }
480
+
481
+        // extract the X-Kolab-Type header from the XML attachment part if missing
482
+        if (empty($message->headers->others['x-kolab-type'])) {
483
+            foreach ((array)$message->attachments as $part) {
484
+                if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) {
485
+                    $message->headers->others['x-kolab-type'] = $part->mimetype;
486
+                    break;
487
+                }
488
+            }
489
+        }
490
+        // fix buggy messages stating the X-Kolab-Type header twice
491
+        else if (is_array($message->headers->others['x-kolab-type'])) {
492
+            $message->headers->others['x-kolab-type'] = reset($message->headers->others['x-kolab-type']);
493
+        }
494
+
495
+        // no object type header found: abort
496
+        if (empty($message->headers->others['x-kolab-type'])) {
497
+            rcube::raise_error(array(
498
+                'code' => 600,
499
+                'type' => 'php',
500
+                'file' => __FILE__,
501
+                'line' => __LINE__,
502
+                'message' => "No X-Kolab-Type information found in message $msguid ($this->name).",
503
+            ), true);
504
+            return false;
505
+        }
506
+
507
+        $object_type  = kolab_format::mime2object_type($message->headers->others['x-kolab-type']);
508
+        $content_type = kolab_format::KTYPE_PREFIX . $object_type;
509
+
510
+        // check object type header and abort on mismatch
511
+        if ($type != '*' && $object_type != $type)
512
+            return false;
513
+
514
+        $attachments = array();
515
+
516
+        // get XML part
517
+        foreach ((array)$message->attachments as $part) {
518
+            if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!', $part->mimetype))) {
519
+                $xml = $message->get_part_body($part->mime_id, true);
520
+            }
521
+            else if ($part->filename || $part->content_id) {
522
+                $key  = $part->content_id ? trim($part->content_id, '<>') : $part->filename;
523
+                $size = null;
524
+
525
+                // Use Content-Disposition 'size' as for the Kolab Format spec.
526
+                if (isset($part->d_parameters['size'])) {
527
+                    $size = $part->d_parameters['size'];
528
+                }
529
+                // we can trust part size only if it's not encoded
530
+                else if ($part->encoding == 'binary' || $part->encoding == '7bit' || $part->encoding == '8bit') {
531
+                    $size = $part->size;
532
+                }
533
+
534
+                $attachments[$key] = array(
535
+                    'id'       => $part->mime_id,
536
+                    'name'     => $part->filename,
537
+                    'mimetype' => $part->mimetype,
538
+                    'size'     => $size,
539
+                );
540
+            }
541
+        }
542
+
543
+        if (!$xml) {
544
+            rcube::raise_error(array(
545
+                'code' => 600,
546
+                'type' => 'php',
547
+                'file' => __FILE__,
548
+                'line' => __LINE__,
549
+                'message' => "Could not find Kolab data part in message $msguid ($this->name).",
550
+            ), true);
551
+            return false;
552
+        }
553
+
554
+        // check kolab format version
555
+        $format_version = $message->headers->others['x-kolab-mime-version'];
556
+        if (empty($format_version)) {
557
+            list($xmltype, $subtype) = explode('.', $object_type);
558
+            $xmlhead = substr($xml, 0, 512);
559
+
560
+            // detect old Kolab 2.0 format
561
+            if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false)
562
+                $format_version = '2.0';
563
+            else
564
+                $format_version = '3.0'; // assume 3.0
565
+        }
566
+
567
+        // get Kolab format handler for the given type
568
+        $format = kolab_format::factory($object_type, $format_version);
569
+
570
+        if (is_a($format, 'PEAR_Error'))
571
+            return false;
572
+
573
+        // load Kolab object from XML part
574
+        $format->load($xml);
575
+
576
+        if ($format->is_valid()) {
577
+            $object = $format->to_array(array('_attachments' => $attachments));
578
+            $object['_type']      = $object_type;
579
+            $object['_msguid']    = $msguid;
580
+            $object['_mailbox']   = $this->name;
581
+            $object['_formatobj'] = $format;
582
+
583
+            return $object;
584
+        }
585
+        else {
586
+            // try to extract object UID from XML block
587
+            if (preg_match('!<uid>(.+)</uid>!Uims', $xml, $m))
588
+                $msgadd = " UID = " . trim(strip_tags($m[1]));
589
+
590
+            rcube::raise_error(array(
591
+                'code' => 600,
592
+                'type' => 'php',
593
+                'file' => __FILE__,
594
+                'line' => __LINE__,
595
+                'message' => "Could not parse Kolab object data in message $msguid ($this->name)." . $msgadd,
596
+            ), true);
597
+        }
598
+
599
+        return false;
600
+    }
601
+
602
+    /**
603
+     * Save an object in this folder.
604
+     *
605
+     * @param array  $object    The array that holds the data of the object.
606
+     * @param string $type      The type of the kolab object.
607
+     * @param string $uid       The UID of the old object if it existed before
608
+     * @return boolean          True on success, false on error
609
+     */
610
+    public function save(&$object, $type = null, $uid = null)
611
+    {
612
+        if (!$this->valid) {
613
+            return false;
614
+        }
615
+
616
+        if (!$type)
617
+            $type = $this->type;
618
+
619
+        // copy attachments from old message
620
+        if (!empty($object['_msguid']) && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) {
621
+            foreach ((array)$old['_attachments'] as $key => $att) {
622
+                if (!isset($object['_attachments'][$key])) {
623
+                    $object['_attachments'][$key] = $old['_attachments'][$key];
624
+                }
625
+                // unset deleted attachment entries
626
+                if ($object['_attachments'][$key] == false) {
627
+                    unset($object['_attachments'][$key]);
628
+                }
629
+                // load photo.attachment from old Kolab2 format to be directly embedded in xcard block
630
+                else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) {
631
+                    if (!isset($object['photo']))
632
+                        $object['photo'] = $this->get_attachment($object['_msguid'], $att['id'], $object['_mailbox']);
633
+                    unset($object['_attachments'][$key]);
634
+                }
635
+            }
636
+        }
637
+
638
+        // save contact photo to attachment for Kolab2 format
639
+        if (kolab_storage::$version == '2.0' && $object['photo']) {
640
+            $attkey = 'kolab-picture.png';  // this file name is hard-coded in libkolab/kolabformatV2/contact.cpp
641
+            $object['_attachments'][$attkey] = array(
642
+                'mimetype'=> rcube_mime::image_content_type($object['photo']),
643
+                'content' => preg_match('![^a-z0-9/=+-]!i', $object['photo']) ? $object['photo'] : base64_decode($object['photo']),
644
+            );
645
+        }
646
+
647
+        // process attachments
648
+        if (is_array($object['_attachments'])) {
649
+            $numatt = count($object['_attachments']);
650
+            foreach ($object['_attachments'] as $key => $attachment) {
651
+                // FIXME: kolab_storage and Roundcube attachment hooks use different fields!
652
+                if (empty($attachment['content']) && !empty($attachment['data'])) {
653
+                    $attachment['content'] = $attachment['data'];
654
+                    unset($attachment['data'], $object['_attachments'][$key]['data']);
655
+                }
656
+
657
+                // make sure size is set, so object saved in cache contains this info
658
+                if (!isset($attachment['size'])) {
659
+                    if (!empty($attachment['content'])) {
660
+                        if (is_resource($attachment['content'])) {
661
+                            // this need to be a seekable resource, otherwise
662
+                            // fstat() failes and we're unable to determine size
663
+                            // here nor in rcube_imap_generic before IMAP APPEND
664
+                            $stat = fstat($attachment['content']);
665
+                            $attachment['size'] = $stat ? $stat['size'] : 0;
666
+                        }
667
+                        else {
668
+                            $attachment['size'] = strlen($attachment['content']);
669
+                        }
670
+                    }
671
+                    else if (!empty($attachment['path'])) {
672
+                        $attachment['size'] = filesize($attachment['path']);
673
+                    }
674
+                    $object['_attachments'][$key] = $attachment;
675
+                }
676
+
677
+                // generate unique keys (used as content-id) for attachments
678
+                if (is_numeric($key) && $key < $numatt) {
679
+                    // derrive content-id from attachment file name
680
+                    $ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null;
681
+                    $basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext));  // to 7bit ascii
682
+                    if (!$basename) $basename = 'noname';
683
+                    $cid = $basename . '.' . microtime(true) . $ext;
684
+
685
+                    $object['_attachments'][$cid] = $attachment;
686
+                    unset($object['_attachments'][$key]);
687
+                }
688
+            }
689
+        }
690
+
691
+        // save recurrence exceptions as individual objects due to lack of support in Kolab v2 format
692
+        if (kolab_storage::$version == '2.0' && $object['recurrence']['EXCEPTIONS']) {
693
+            $this->save_recurrence_exceptions($object, $type);
694
+        }
695
+
696
+        // check IMAP BINARY extension support for 'file' objects
697
+        // allow configuration to workaround bug in Cyrus < 2.4.17
698
+        $rcmail = rcube::get_instance();
699
+        $binary = $type == 'file' && !$rcmail->config->get('kolab_binary_disable') && $this->imap->get_capability('BINARY');
700
+
701
+        // generate and save object message
702
+        if ($raw_msg = $this->build_message($object, $type, $binary, $body_file)) {
703
+            // resolve old msguid before saving
704
+            if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) {
705
+                $object['_msguid'] = $msguid;
706
+                $object['_mailbox'] = $this->name;
707
+            }
708
+
709
+            $result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary);
710
+
711
+            // update cache with new UID
712
+            if ($result) {
713
+                $old_uid = $object['_msguid'];
714
+
715
+                $object['_msguid'] = $result;
716
+                $object['_mailbox'] = $this->name;
717
+
718
+                if ($old_uid) {
719
+                    // delete old message
720
+                    $this->cache->bypass(true);
721
+                    $this->imap->delete_message($old_uid, $object['_mailbox']);
722
+                    $this->cache->bypass(false);
723
+                }
724
+
725
+                // insert/update message in cache
726
+                $this->cache->save($result, $object, $old_uid);
727
+            }
728
+
729
+            // remove temp file
730
+            if ($body_file) {
731
+                @unlink($body_file);
732
+            }
733
+        }
734
+
735
+        return $result;
736
+    }
737
+
738
+    /**
739
+     * Save recurrence exceptions as individual objects.
740
+     * The Kolab v2 format doesn't allow us to save fully embedded exception objects.
741
+     *
742
+     * @param array Hash array with event properties
743
+     * @param string Object type
744
+     */
745
+    private function save_recurrence_exceptions(&$object, $type = null)
746
+    {
747
+        if ($object['recurrence']['EXCEPTIONS']) {
748
+            $exdates = array();
749
+            foreach ((array)$object['recurrence']['EXDATE'] as $exdate) {
750
+                $key = is_a($exdate, 'DateTime') ? $exdate->format('Y-m-d') : strval($exdate);
751
+                $exdates[$key] = 1;
752
+            }
753
+
754
+            // save every exception as individual object
755
+            foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
756
+                $exception['uid'] = self::recurrence_exception_uid($object['uid'], $exception['start']->format('Ymd'));
757
+                $exception['sequence'] = $object['sequence'] + 1;
758
+
759
+                if ($exception['thisandfuture']) {
760
+                    $exception['recurrence'] = $object['recurrence'];
761
+
762
+                    // adjust the recurrence duration of the exception
763
+                    if ($object['recurrence']['COUNT']) {
764
+                        $recurrence = new kolab_date_recurrence($object['_formatobj']);
765
+                        if ($end = $recurrence->end()) {
766
+                            unset($exception['recurrence']['COUNT']);
767
+                            $exception['recurrence']['UNTIL'] = $end;
768
+                        }
769
+                    }
770
+
771
+                    // set UNTIL date if we have a thisandfuture exception
772
+                    $untildate = clone $exception['start'];
773
+                    $untildate->sub(new DateInterval('P1D'));
774
+                    $object['recurrence']['UNTIL'] = $untildate;
775
+                    unset($object['recurrence']['COUNT']);
776
+                }
777
+                else {
778
+                    if (!$exdates[$exception['start']->format('Y-m-d')])
779
+                        $object['recurrence']['EXDATE'][] = clone $exception['start'];
780
+                    unset($exception['recurrence']);
781
+                }
782
+
783
+                unset($exception['recurrence']['EXCEPTIONS'], $exception['_formatobj'], $exception['_msguid']);
784
+                $this->save($exception, $type, $exception['uid']);
785
+            }
786
+
787
+            unset($object['recurrence']['EXCEPTIONS']);
788
+        }
789
+    }
790
+
791
+    /**
792
+     * Generate an object UID with the given recurrence-ID in a way that it is
793
+     * unique (the original UID is not a substring) but still recoverable.
794
+     */
795
+    private static function recurrence_exception_uid($uid, $recurrence_id)
796
+    {
797
+        $offset = -2;
798
+        return substr($uid, 0, $offset) . '-' . $recurrence_id . '-' . substr($uid, $offset);
799
+    }
800
+
801
+    /**
802
+     * Delete the specified object from this folder.
803
+     *
804
+     * @param  mixed   $object  The Kolab object to delete or object UID
805
+     * @param  boolean $expunge Should the folder be expunged?
806
+     *
807
+     * @return boolean True if successful, false on error
808
+     */
809
+    public function delete($object, $expunge = true)
810
+    {
811
+        if (!$this->valid) {
812
+            return false;
813
+        }
814
+
815
+        $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
816
+        $success = false;
817
+
818
+        $this->cache->bypass(true);
819
+
820
+        if ($msguid && $expunge) {
821
+            $success = $this->imap->delete_message($msguid, $this->name);
822
+        }
823
+        else if ($msguid) {
824
+            $success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
825
+        }
826
+
827
+        $this->cache->bypass(false);
828
+
829
+        if ($success) {
830
+            $this->cache->set($msguid, false);
831
+        }
832
+
833
+        return $success;
834
+    }
835
+
836
+
837
+    /**
838
+     *
839
+     */
840
+    public function delete_all()
841
+    {
842
+        if (!$this->valid) {
843
+            return false;
844
+        }
845
+
846
+        $this->cache->purge();
847
+        $this->cache->bypass(true);
848
+        $result = $this->imap->clear_folder($this->name);
849
+        $this->cache->bypass(false);
850
+
851
+        return $result;
852
+    }
853
+
854
+
855
+    /**
856
+     * Restore a previously deleted object
857
+     *
858
+     * @param string Object UID
859
+     * @return mixed Message UID on success, false on error
860
+     */
861
+    public function undelete($uid)
862
+    {
863
+        if (!$this->valid) {
864
+            return false;
865
+        }
866
+
867
+        if ($msguid = $this->cache->uid2msguid($uid, true)) {
868
+            $this->cache->bypass(true);
869
+            $result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name);
870
+            $this->cache->bypass(false);
871
+
872
+            if ($result) {
873
+                return $msguid;
874
+            }
875
+        }
876
+
877
+        return false;
878
+    }
879
+
880
+
881
+    /**
882
+     * Move a Kolab object message to another IMAP folder
883
+     *
884
+     * @param string Object UID
885
+     * @param string IMAP folder to move object to
886
+     * @return boolean True on success, false on failure
887
+     */
888
+    public function move($uid, $target_folder)
889
+    {
890
+        if (!$this->valid) {
891
+            return false;
892
+        }
893
+
894
+        if (is_string($target_folder))
895
+            $target_folder = kolab_storage::get_folder($target_folder);
896
+
897
+        if ($msguid = $this->cache->uid2msguid($uid)) {
898
+            $this->cache->bypass(true);
899
+            $result = $this->imap->move_message($msguid, $target_folder->name, $this->name);
900
+            $this->cache->bypass(false);
901
+
902
+            if ($result) {
903
+                $this->cache->move($msguid, $uid, $target_folder);
904
+                return true;
905
+            }
906
+            else {
907
+                rcube::raise_error(array(
908
+                    'code' => 600, 'type' => 'php',
909
+                    'file' => __FILE__, 'line' => __LINE__,
910
+                    'message' => "Failed to move message $msguid to $target_folder: " . $this->imap->get_error_str(),
911
+                ), true);
912
+            }
913
+        }
914
+
915
+        return false;
916
+    }
917
+
918
+
919
+    /**
920
+     * Creates source of the configuration object message
921
+     *
922
+     * @param array  $object    The array that holds the data of the object.
923
+     * @param string $type      The type of the kolab object.
924
+     * @param bool   $binary    Enables use of binary encoding of attachment(s)
925
+     * @param string $body_file Reference to filename of message body
926
+     *
927
+     * @return mixed Message as string or array with two elements
928
+     *               (one for message file path, second for message headers)
929
+     */
930
+    private function build_message(&$object, $type, $binary, &$body_file)
931
+    {
932
+        // load old object to preserve data we don't understand/process
933
+        if (is_object($object['_formatobj']))
934
+            $format = $object['_formatobj'];
935
+        else if ($object['_msguid'] && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox'])))
936
+            $format = $old['_formatobj'];
937
+
938
+        // create new kolab_format instance
939
+        if (!$format)
940
+            $format = kolab_format::factory($type, kolab_storage::$version);
941
+
942
+        if (PEAR::isError($format))
943
+            return false;
944
+
945
+        $format->set($object);
946
+        $xml = $format->write(kolab_storage::$version);
947
+        $object['uid'] = $format->uid;  // read UID from format
948
+        $object['_formatobj'] = $format;
949
+
950
+        if (empty($xml) || !$format->is_valid() || empty($object['uid'])) {
951
+            return false;
952
+        }
953
+
954
+        $mime     = new Mail_mime("\r\n");
955
+        $rcmail   = rcube::get_instance();
956
+        $headers  = array();
957
+        $files    = array();
958
+        $part_id  = 1;
959
+        $encoding = $binary ? 'binary' : 'base64';
960
+
961
+        if ($user_email = $rcmail->get_user_email()) {
962
+            $headers['From'] = $user_email;
963
+            $headers['To'] = $user_email;
964
+        }
965
+        $headers['Date'] = date('r');
966
+        $headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type;
967
+        $headers['X-Kolab-Mime-Version'] = kolab_storage::$version;
968
+        $headers['Subject'] = $object['uid'];
969
+//        $headers['Message-ID'] = $rcmail->gen_message_id();
970
+        $headers['User-Agent'] = $rcmail->config->get('useragent');
971
+
972
+        // Check if we have enough memory to handle the message in it
973
+        // It's faster than using files, so we'll do this if we only can
974
+        if (!empty($object['_attachments']) && ($mem_limit = parse_bytes(ini_get('memory_limit'))) > 0) {
975
+            $memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB
976
+
977
+            foreach ($object['_attachments'] as $attachment) {
978
+                $memory += $attachment['size'];
979
+            }
980
+
981
+            // 1.33 is for base64, we need at least 4x more memory than the message size
982
+            if ($memory * ($binary ? 1 : 1.33) * 4 > $mem_limit) {
983
+                $marker   = '%%%~~~' . md5(microtime(true) . $memory) . '~~~%%%';
984
+                $is_file  = true;
985
+                $temp_dir = unslashify($rcmail->config->get('temp_dir'));
986
+                $mime->setParam('delay_file_io', true);
987
+            }
988
+        }
989
+
990
+        $mime->headers($headers);
991
+        $mime->setTXTBody("This is a Kolab Groupware object. "
992
+            . "To view this object you will need an email client that understands the Kolab Groupware format. "
993
+            . "For a list of such email clients please visit http://www.kolab.org/\n\n");
994
+
995
+        $ctype = kolab_storage::$version == '2.0' ? $format->CTYPEv2 : $format->CTYPE;
996
+        // Convert new lines to \r\n, to wrokaround "NO Message contains bare newlines"
997
+        // when APPENDing from temp file
998
+        $xml = preg_replace('/\r?\n/', "\r\n", $xml);
999
+
1000
+        $mime->addAttachment($xml,  // file
1001
+            $ctype,                 // content-type
1002
+            'kolab.xml',            // filename
1003
+            false,                  // is_file
1004
+            '8bit',                 // encoding
1005
+            'attachment',           // disposition
1006
+            RCUBE_CHARSET           // charset
1007
+        );
1008
+        $part_id++;
1009
+
1010
+        // save object attachments as separate parts
1011
+        foreach ((array)$object['_attachments'] as $key => $att) {
1012
+            if (empty($att['content']) && !empty($att['id'])) {
1013
+                // @TODO: use IMAP CATENATE to skip attachment fetch+push operation
1014
+                $msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
1015
+                if ($is_file) {
1016
+                    $att['path'] = tempnam($temp_dir, 'rcmAttmnt');
1017
+                    if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) {
1018
+                        fclose($fp);
1019
+                    }
1020
+                    else {
1021
+                        return false;
1022
+                    }
1023
+                }
1024
+                else {
1025
+                    $att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, null, true);
1026
+                }
1027
+            }
1028
+
1029
+            $headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCUBE_CHARSET, 'quoted-printable'));
1030
+            $name = !empty($att['name']) ? $att['name'] : $key;
1031
+
1032
+            // To store binary files we can use faster method
1033
+            // without writting full message content to a temporary file but
1034
+            // directly to IMAP, see rcube_imap_generic::append().
1035
+            // I.e. use file handles where possible
1036
+            if (!empty($att['path'])) {
1037
+                if ($is_file && $binary) {
1038
+                    $files[] = fopen($att['path'], 'r');
1039
+                    $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
1040
+                }
1041
+                else {
1042
+                    $mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
1043
+                }
1044
+            }
1045
+            else {
1046
+                if (is_resource($att['content']) && $is_file && $binary) {
1047
+                    $files[] = $att['content'];
1048
+                    $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
1049
+                }
1050
+                else {
1051
+                    if (is_resource($att['content'])) {
1052
+                        @rewind($att['content']);
1053
+                        $att['content'] = stream_get_contents($att['content']);
1054
+                    }
1055
+                    $mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
1056
+                }
1057
+            }
1058
+
1059
+            $object['_attachments'][$key]['id'] = ++$part_id;
1060
+        }
1061
+
1062
+        if (!$is_file || !empty($files)) {
1063
+            $message = $mime->getMessage();
1064
+        }
1065
+
1066
+        // parse message and build message array with
1067
+        // attachment file pointers in place of file markers
1068
+        if (!empty($files)) {
1069
+            $message = explode($marker, $message);
1070
+            $tmp     = array();
1071
+
1072
+            foreach ($message as $msg_part) {
1073
+                $tmp[] = $msg_part;
1074
+                if ($file = array_shift($files)) {
1075
+                    $tmp[] = $file;
1076
+                }
1077
+            }
1078
+            $message = $tmp;
1079
+        }
1080
+        // write complete message body into temp file
1081
+        else if ($is_file) {
1082
+            // use common temp dir
1083
+            $body_file = tempnam($temp_dir, 'rcmMsg');
1084
+
1085
+            if (PEAR::isError($mime_result = $mime->saveMessageBody($body_file))) {
1086
+                self::raise_error(array('code' => 650, 'type' => 'php',
1087
+                    'file' => __FILE__, 'line' => __LINE__,
1088
+                    'message' => "Could not create message: ".$mime_result->getMessage()),
1089
+                    true, false);
1090
+                return false;
1091
+            }
1092
+
1093
+            $message = array(trim($mime->txtHeaders()) . "\r\n\r\n", fopen($body_file, 'r'));
1094
+        }
1095
+
1096
+        return $message;
1097
+    }
1098
+
1099
+
1100
+    /**
1101
+     * Triggers any required updates after changes within the
1102
+     * folder. This is currently only required for handling free/busy
1103
+     * information with Kolab.
1104
+     *
1105
+     * @return boolean|PEAR_Error True if successfull.
1106
+     */
1107
+    public function trigger()
1108
+    {
1109
+        $owner = $this->get_owner();
1110
+        $result = false;
1111
+
1112
+        switch($this->type) {
1113
+        case 'event':
1114
+            if ($this->get_namespace() == 'personal') {
1115
+                $result = $this->trigger_url(
1116
+                    sprintf('%s/trigger/%s/%s.pfb',
1117
+                        kolab_storage::get_freebusy_server(),
1118
+                        urlencode($owner),
1119
+                        urlencode($this->imap->mod_folder($this->name))
1120
+                    ),
1121
+                    $this->imap->options['user'],
1122
+                    $this->imap->options['password']
1123
+                );
1124
+            }
1125
+            break;
1126
+
1127
+        default:
1128
+            return true;
1129
+        }
1130
+
1131
+        if ($result && is_object($result) && is_a($result, 'PEAR_Error')) {
1132
+            return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s",
1133
+                                            $this->name, $result->getMessage()));
1134
+        }
1135
+
1136
+        return $result;
1137
+    }
1138
+
1139
+    /**
1140
+     * Triggers a URL.
1141
+     *
1142
+     * @param string $url          The URL to be triggered.
1143
+     * @param string $auth_user    Username to authenticate with
1144
+     * @param string $auth_passwd  Password for basic auth
1145
+     * @return boolean|PEAR_Error  True if successfull.
1146
+     */
1147
+    private function trigger_url($url, $auth_user = null, $auth_passwd = null)
1148
+    {
1149
+        try {
1150
+            $request = libkolab::http_request($url);
1151
+
1152
+            // set authentication credentials
1153
+            if ($auth_user && $auth_passwd)
1154
+                $request->setAuth($auth_user, $auth_passwd);
1155
+
1156
+            $result = $request->send();
1157
+            // rcube::write_log('trigger', $result->getBody());
1158
+        }
1159
+        catch (Exception $e) {
1160
+            return PEAR::raiseError($e->getMessage());
1161
+        }
1162
+
1163
+        return true;
1164
+    }
1165
+
1166
+}
1167
+
1168
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_api.php Added
353
 
1
@@ -0,0 +1,351 @@
2
+<?php
3
+
4
+/**
5
+ * Abstract interface class for Kolab storage IMAP folder objects
6
+ *
7
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
8
+ *
9
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
10
+ *
11
+ * This program is free software: you can redistribute it and/or modify
12
+ * it under the terms of the GNU Affero General Public License as
13
+ * published by the Free Software Foundation, either version 3 of the
14
+ * License, or (at your option) any later version.
15
+ *
16
+ * This program is distributed in the hope that it will be useful,
17
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ * GNU Affero General Public License for more details.
20
+ *
21
+ * You should have received a copy of the GNU Affero General Public License
22
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
+ */
24
+abstract class kolab_storage_folder_api
25
+{
26
+    /**
27
+     * Folder identifier
28
+     * @var string
29
+     */
30
+    public $id;
31
+
32
+    /**
33
+     * The folder name.
34
+     * @var string
35
+     */
36
+    public $name;
37
+
38
+    /**
39
+     * The type of this folder.
40
+     * @var string
41
+     */
42
+    public $type;
43
+
44
+    /**
45
+     * The subtype of this folder.
46
+     * @var string
47
+     */
48
+    public $subtype;
49
+
50
+    /**
51
+     * Is this folder set to be the default for its type
52
+     * @var boolean
53
+     */
54
+    public $default = false;
55
+
56
+    /**
57
+     * List of direct child folders
58
+     * @var array
59
+     */
60
+    public $children = array();
61
+    
62
+    /**
63
+     * Name of the parent folder
64
+     * @var string
65
+     */
66
+    public $parent = '';
67
+
68
+    protected $imap;
69
+    protected $owner;
70
+    protected $info;
71
+    protected $idata;
72
+    protected $namespace;
73
+
74
+
75
+    /**
76
+     * Private constructor
77
+     */
78
+    protected function __construct($name)
79
+    {
80
+      $this->name = $name;
81
+      $this->id   = kolab_storage::folder_id($name);
82
+      $this->imap = rcube::get_instance()->get_storage();
83
+    }
84
+
85
+
86
+    /**
87
+     * Returns the owner of the folder.
88
+     *
89
+     * @param boolean  Return a fully qualified owner name (i.e. including domain for shared folders)
90
+     * @return string  The owner of this folder.
91
+     */
92
+    public function get_owner($fully_qualified = false)
93
+    {
94
+        // return cached value
95
+        if (isset($this->owner))
96
+            return $this->owner;
97
+
98
+        $info = $this->get_folder_info();
99
+        $rcmail = rcube::get_instance();
100
+
101
+        switch ($info['namespace']) {
102
+        case 'personal':
103
+            $this->owner = $rcmail->get_user_name();
104
+            break;
105
+
106
+        case 'shared':
107
+            $this->owner = 'anonymous';
108
+            break;
109
+
110
+        default:
111
+            list($prefix, $this->owner) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
112
+            $fully_qualified = true;  // enforce email addresses (backwards compatibility)
113
+            break;
114
+        }
115
+
116
+        if ($fully_qualified && strpos($this->owner, '@') === false) {
117
+            // extract domain from current user name
118
+            $domain = strstr($rcmail->get_user_name(), '@');
119
+            // fall back to mail_domain config option
120
+            if (empty($domain) && ($mdomain = $rcmail->config->mail_domain($this->imap->options['host']))) {
121
+                $domain = '@' . $mdomain;
122
+            }
123
+            $this->owner .= $domain;
124
+        }
125
+
126
+        return $this->owner;
127
+    }
128
+
129
+
130
+    /**
131
+     * Getter for the name of the namespace to which the IMAP folder belongs
132
+     *
133
+     * @return string Name of the namespace (personal, other, shared)
134
+     */
135
+    public function get_namespace()
136
+    {
137
+        if (!isset($this->namespace))
138
+            $this->namespace = $this->imap->folder_namespace($this->name);
139
+        return $this->namespace;
140
+    }
141
+
142
+
143
+    /**
144
+     * Get the display name value of this folder
145
+     *
146
+     * @return string Folder name
147
+     */
148
+    public function get_name()
149
+    {
150
+        return kolab_storage::object_name($this->name, $this->get_namespace());
151
+    }
152
+
153
+
154
+    /**
155
+     * Getter for the top-end folder name (not the entire path)
156
+     *
157
+     * @return string Name of this folder
158
+     */
159
+    public function get_foldername()
160
+    {
161
+        $parts = explode('/', $this->name);
162
+        return rcube_charset::convert(end($parts), 'UTF7-IMAP');
163
+    }
164
+
165
+    /**
166
+     * Getter for parent folder path
167
+     *
168
+     * @return string Full path to parent folder
169
+     */
170
+    public function get_parent()
171
+    {
172
+        $path = explode('/', $this->name);
173
+        array_pop($path);
174
+
175
+        // don't list top-level namespace folder
176
+        if (count($path) == 1 && in_array($this->get_namespace(), array('other', 'shared'))) {
177
+            $path = array();
178
+        }
179
+
180
+        return join('/', $path);
181
+    }
182
+
183
+    /**
184
+     * Getter for the Cyrus mailbox identifier corresponding to this folder
185
+     * (e.g. user/john.doe/Calendar/Personal@example.org)
186
+     *
187
+     * @return string Mailbox ID
188
+     */
189
+    public function get_mailbox_id()
190
+    {
191
+        $info = $this->get_folder_info();
192
+        $owner = $this->get_owner();
193
+        list($user, $domain) = explode('@', $owner);
194
+
195
+        switch ($info['namespace']) {
196
+        case 'personal':
197
+            return sprintf('user/%s/%s@%s', $user, $this->name, $domain);
198
+
199
+        case 'shared':
200
+            $ns = $this->imap->get_namespace('shared');
201
+            $prefix = is_array($ns) ? $ns[0][0] : '';
202
+            list(, $domain) = explode('@', rcube::get_instance()->get_user_name());
203
+            return substr($this->name, strlen($prefix)) . '@' . $domain;
204
+
205
+        default:
206
+            $ns = $this->imap->get_namespace('other');
207
+            $prefix = is_array($ns) ? $ns[0][0] : '';
208
+            list($user, $folder) = explode($this->imap->get_hierarchy_delimiter(), substr($info['name'], strlen($prefix)), 2);
209
+            if (strpos($user, '@')) {
210
+                list($user, $domain) = explode('@', $user);
211
+            }
212
+            return sprintf('user/%s/%s@%s', $user, $folder, $domain);
213
+        }
214
+    }
215
+
216
+    /**
217
+     * Get the color value stored in metadata
218
+     *
219
+     * @param string Default color value to return if not set
220
+     * @return mixed Color value from IMAP metadata or $default is not set
221
+     */
222
+    public function get_color($default = null)
223
+    {
224
+        // color is defined in folder METADATA
225
+        $metadata = $this->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
226
+        if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
227
+            return $color;
228
+        }
229
+
230
+        return $default;
231
+    }
232
+
233
+
234
+    /**
235
+     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
236
+     *
237
+     * @param array List of metadata keys to read
238
+     * @return array Metadata entry-value hash array on success, NULL on error
239
+     */
240
+    public function get_metadata($keys)
241
+    {
242
+        $metadata = rcube::get_instance()->get_storage()->get_metadata($this->name, (array)$keys);
243
+        return $metadata[$this->name];
244
+    }
245
+
246
+
247
+    /**
248
+     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
249
+     *
250
+     * @param array  $entries Entry-value array (use NULL value as NIL)
251
+     * @return boolean True on success, False on failure
252
+     */
253
+    public function set_metadata($entries)
254
+    {
255
+        return $this->imap->set_metadata($this->name, $entries);
256
+    }
257
+
258
+
259
+    /**
260
+     *
261
+     */
262
+    public function get_folder_info()
263
+    {
264
+        if (!isset($this->info))
265
+            $this->info = $this->imap->folder_info($this->name);
266
+
267
+        return $this->info;
268
+    }
269
+
270
+    /**
271
+     * Make IMAP folder data available for this folder
272
+     */
273
+    public function get_imap_data()
274
+    {
275
+        if (!isset($this->idata))
276
+            $this->idata = $this->imap->folder_data($this->name);
277
+
278
+        return $this->idata;
279
+    }
280
+
281
+
282
+    /**
283
+     * Get IMAP ACL information for this folder
284
+     *
285
+     * @return string  Permissions as string
286
+     */
287
+    public function get_myrights()
288
+    {
289
+        $rights = $this->info['rights'];
290
+
291
+        if (!is_array($rights))
292
+            $rights = $this->imap->my_rights($this->name);
293
+
294
+        return join('', (array)$rights);
295
+    }
296
+
297
+
298
+    /**
299
+     * Check activation status of this folder
300
+     *
301
+     * @return boolean True if enabled, false if not
302
+     */
303
+    public function is_active()
304
+    {
305
+        return kolab_storage::folder_is_active($this->name);
306
+    }
307
+
308
+    /**
309
+     * Change activation status of this folder
310
+     *
311
+     * @param boolean The desired subscription status: true = active, false = not active
312
+     *
313
+     * @return True on success, false on error
314
+     */
315
+    public function activate($active)
316
+    {
317
+        return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
318
+    }
319
+
320
+    /**
321
+     * Check subscription status of this folder
322
+     *
323
+     * @return boolean True if subscribed, false if not
324
+     */
325
+    public function is_subscribed()
326
+    {
327
+        return kolab_storage::folder_is_subscribed($this->name);
328
+    }
329
+
330
+    /**
331
+     * Change subscription status of this folder
332
+     *
333
+     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
334
+     *
335
+     * @return True on success, false on error
336
+     */
337
+    public function subscribe($subscribed)
338
+    {
339
+        return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
340
+    }
341
+
342
+    /**
343
+     * Return folder name as string representation of this object
344
+     *
345
+     * @return string Full IMAP folder name
346
+     */
347
+    public function __toString()
348
+    {
349
+        return $this->name;
350
+    }
351
+}
352
+
353
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_user.php Added
138
 
1
@@ -0,0 +1,135 @@
2
+<?php
3
+
4
+/**
5
+ * Class that represents a (virtual) folder in the 'other' namespace
6
+ * implementing a subset of the kolab_storage_folder API.
7
+ *
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+class kolab_storage_folder_user extends kolab_storage_folder_virtual
26
+{
27
+    protected static $ldapcache = array();
28
+
29
+    public $ldaprec;
30
+    public $type;
31
+
32
+    /**
33
+     * Default constructor
34
+     */
35
+    public function __construct($name, $parent = '', $ldaprec = null)
36
+    {
37
+        parent::__construct($name, $name, 'other', $parent);
38
+
39
+        if (!empty($ldaprec)) {
40
+            self::$ldapcache[$name] = $this->ldaprec = $ldaprec;
41
+        }
42
+        // use value cached in memory for repeated lookups
43
+        else if (array_key_exists($name, self::$ldapcache)) {
44
+            $this->ldaprec = self::$ldapcache[$name];
45
+        }
46
+        // lookup user in LDAP and set $this->ldaprec
47
+        else if ($ldap = kolab_storage::ldap()) {
48
+            // get domain from current user
49
+            list(,$domain) = explode('@', rcube::get_instance()->get_user_name());
50
+            $this->ldaprec = $ldap->get_user_record(parent::get_foldername($this->name) . '@' . $domain, $_SESSION['imap_host']);
51
+            if (!empty($this->ldaprec)) {
52
+                $this->ldaprec['kolabtargetfolder'] = $name;
53
+            }
54
+            self::$ldapcache[$name] = $this->ldaprec;
55
+        }
56
+    }
57
+
58
+    /**
59
+     * Getter for the top-end folder name to be displayed
60
+     *
61
+     * @return string Name of this folder
62
+     */
63
+    public function get_foldername()
64
+    {
65
+        return $this->ldaprec ? ($this->ldaprec['displayname'] ?: $this->ldaprec['name']) :
66
+            parent::get_foldername();
67
+    }
68
+
69
+    /**
70
+     * Getter for a more informative title of this user folder
71
+     *
72
+     * @return string Title for the given user record
73
+     */
74
+    public function get_title()
75
+    {
76
+      return trim($this->ldaprec['displayname'] . '; ' . $this->ldaprec['mail'], '; ');
77
+    }
78
+
79
+    /**
80
+     * Returns the owner of the folder.
81
+     *
82
+     * @return string  The owner of this folder.
83
+     */
84
+    public function get_owner()
85
+    {
86
+        return $this->ldaprec['mail'];
87
+    }
88
+
89
+    /**
90
+     * Check subscription status of this folder.
91
+     * Subscription of a virtual user folder depends on the subscriptions of subfolders.
92
+     *
93
+     * @return boolean True if subscribed, false if not
94
+     */
95
+    public function is_subscribed()
96
+    {
97
+        if (!empty($this->type)) {
98
+            $children = $subscribed = 0;
99
+            $delimiter = $this->imap->get_hierarchy_delimiter();
100
+            foreach ((array)kolab_storage::list_folders($this->name . $delimiter, '*', $this->type, false) as $subfolder) {
101
+                if (kolab_storage::folder_is_subscribed($subfolder)) {
102
+                    $subscribed++;
103
+                }
104
+                $children++;
105
+            }
106
+            if ($subscribed > 0) {
107
+                return $subscribed == $children ? true : 2;
108
+            }
109
+        }
110
+
111
+        return false;
112
+    }
113
+
114
+    /**
115
+     * Change subscription status of this folder
116
+     *
117
+     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
118
+     *
119
+     * @return True on success, false on error
120
+     */
121
+    public function subscribe($subscribed)
122
+    {
123
+        $success = false;
124
+
125
+        // (un)subscribe all subfolders of a given type
126
+        if (!empty($this->type)) {
127
+            $delimiter = $this->imap->get_hierarchy_delimiter();
128
+            foreach ((array)kolab_storage::list_folders($this->name . $delimiter, '*', $this->type, false) as $subfolder) {
129
+                $success |= ($subscribed ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder));
130
+            }
131
+        }
132
+
133
+        return $success;
134
+    }
135
+
136
+}
137
\ No newline at end of file
138
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_virtual.php Added
62
 
1
@@ -0,0 +1,59 @@
2
+<?php
3
+
4
+/**
5
+ * Helper class that represents a virtual IMAP folder
6
+ * with a subset of the kolab_storage_folder API.
7
+ *
8
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
9
+ *
10
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Affero General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Affero General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ */
25
+class kolab_storage_folder_virtual extends kolab_storage_folder_api
26
+{
27
+    public $virtual = true;
28
+
29
+    protected $displayname;
30
+
31
+    public function __construct($name, $dispname, $ns, $parent = '')
32
+    {
33
+        parent::__construct($name);
34
+
35
+        $this->namespace = $ns;
36
+        $this->parent    = $parent;
37
+        $this->displayname = $dispname;
38
+    }
39
+
40
+    /**
41
+     * Get the display name value of this folder
42
+     *
43
+     * @return string Folder name
44
+     */
45
+    public function get_name()
46
+    {
47
+        return $this->displayname ?: parent::get_name();
48
+    }
49
+
50
+    /**
51
+     * Get the color value stored in metadata
52
+     *
53
+     * @param string Default color value to return if not set
54
+     * @return mixed Color value from IMAP metadata or $default is not set
55
+     */
56
+    public function get_color($default = null)
57
+    {
58
+        return $default;
59
+    }
60
+}
61
\ No newline at end of file
62
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/libkolab.php Added
140
 
1
@@ -0,0 +1,138 @@
2
+<?php
3
+
4
+/**
5
+ * Kolab core library
6
+ *
7
+ * Plugin to setup a basic environment for the interaction with a Kolab server.
8
+ * Other Kolab-related plugins will depend on it and can use the library classes
9
+ *
10
+ * @version @package_version@
11
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
12
+ *
13
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
14
+ *
15
+ * This program is free software: you can redistribute it and/or modify
16
+ * it under the terms of the GNU Affero General Public License as
17
+ * published by the Free Software Foundation, either version 3 of the
18
+ * License, or (at your option) any later version.
19
+ *
20
+ * This program is distributed in the hope that it will be useful,
21
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
+ * GNU Affero General Public License for more details.
24
+ *
25
+ * You should have received a copy of the GNU Affero General Public License
26
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
27
+ */
28
+
29
+class libkolab extends rcube_plugin
30
+{
31
+    static $http_requests = array();
32
+
33
+    /**
34
+     * Required startup method of a Roundcube plugin
35
+     */
36
+    public function init()
37
+    {
38
+        // load local config
39
+        $this->load_config();
40
+
41
+        // extend include path to load bundled lib classes
42
+        $include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
43
+        set_include_path($include_path);
44
+
45
+        $this->add_hook('storage_init', array($this, 'storage_init'));
46
+        $this->add_hook('user_delete', array('kolab_storage', 'delete_user_folders'));
47
+
48
+        $rcmail = rcube::get_instance();
49
+        try {
50
+            kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
51
+        }
52
+        catch (Exception $e) {
53
+            rcube::raise_error($e, true);
54
+            kolab_format::$timezone = new DateTimeZone('GMT');
55
+        }
56
+    }
57
+
58
+    /**
59
+     * Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers
60
+     */
61
+    function storage_init($p)
62
+    {
63
+        $p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE X-KOLAB-MIME-VERSION');
64
+        return $p;
65
+    }
66
+
67
+    /**
68
+     * Wrapper function to load and initalize the HTTP_Request2 Object
69
+     *
70
+     * @param string|Net_Url2 Request URL
71
+     * @param string          Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT')
72
+     * @param array           Configuration for this Request instance, that will be merged
73
+     *                        with default configuration
74
+     *
75
+     * @return HTTP_Request2 Request object
76
+     */
77
+    public static function http_request($url = '', $method = 'GET', $config = array())
78
+    {
79
+        $rcube       = rcube::get_instance();
80
+        $http_config = (array) $rcube->config->get('kolab_http_request');
81
+
82
+        // deprecated configuration options
83
+        if (empty($http_config)) {
84
+            foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) {
85
+                $value = $rcube->config->get('kolab_' . $option, true);
86
+                if (is_bool($value)) {
87
+                    $http_config[$option] = $value;
88
+                }
89
+            }
90
+        }
91
+
92
+        if (!empty($config)) {
93
+            $http_config = array_merge($http_config, $config);
94
+        }
95
+
96
+        $key = md5(serialize($http_config));
97
+
98
+        if (!($request = self::$http_requests[$key])) {
99
+            // load HTTP_Request2
100
+            require_once 'HTTP/Request2.php';
101
+
102
+            try {
103
+                $request = new HTTP_Request2();
104
+                $request->setConfig($http_config);
105
+            }
106
+            catch (Exception $e) {
107
+                rcube::raise_error($e, true, true);
108
+            }
109
+
110
+            // proxy User-Agent string
111
+            $request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
112
+
113
+            self::$http_requests[$key] = $request;
114
+        }
115
+
116
+        // cleanup
117
+        try {
118
+            $request->setBody('');
119
+            $request->setUrl($url);
120
+            $request->setMethod($method);
121
+        }
122
+        catch (Exception $e) {
123
+            rcube::raise_error($e, true, true);
124
+        }
125
+
126
+        return $request;
127
+    }
128
+
129
+    /**
130
+     * Wrapper function for generating a html diff using the FineDiff class by Raymond Hill
131
+     */
132
+    public static function html_diff($from, $to)
133
+    {
134
+      include_once __dir__ . '/vendor/finediff.php';
135
+
136
+      $diff = new FineDiff($from, $to, FineDiff::$wordGranularity);
137
+      return $diff->renderDiffToHTML();
138
+    }
139
+}
140
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/vendor Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/vendor/finediff.php Added
690
 
1
@@ -0,0 +1,688 @@
2
+<?php
3
+/**
4
+* FINE granularity DIFF
5
+*
6
+* Computes a set of instructions to convert the content of
7
+* one string into another.
8
+*
9
+* Copyright (c) 2011 Raymond Hill (http://raymondhill.net/blog/?p=441)
10
+*
11
+* Licensed under The MIT License
12
+* 
13
+* Permission is hereby granted, free of charge, to any person obtaining a copy
14
+* of this software and associated documentation files (the "Software"), to deal
15
+* in the Software without restriction, including without limitation the rights
16
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+* copies of the Software, and to permit persons to whom the Software is
18
+* furnished to do so, subject to the following conditions:
19
+*
20
+* The above copyright notice and this permission notice shall be included in
21
+* all copies or substantial portions of the Software.
22
+*
23
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29
+* THE SOFTWARE.
30
+*
31
+* @copyright Copyright 2011 (c) Raymond Hill (http://raymondhill.net/blog/?p=441)
32
+* @link http://www.raymondhill.net/finediff/
33
+* @version 0.6
34
+* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
35
+*/
36
+
37
+/**
38
+* Usage (simplest):
39
+*
40
+*   include 'finediff.php';
41
+*
42
+*   // for the stock stack, granularity values are:
43
+*   // FineDiff::$paragraphGranularity = paragraph/line level
44
+*   // FineDiff::$sentenceGranularity = sentence level
45
+*   // FineDiff::$wordGranularity = word level
46
+*   // FineDiff::$characterGranularity = character level [default]
47
+*
48
+*   $opcodes = FineDiff::getDiffOpcodes($from_text, $to_text [, $granularityStack = null] );
49
+*   // store opcodes for later use...
50
+*
51
+*   ...
52
+*
53
+*   // restore $to_text from $from_text + $opcodes
54
+*   include 'finediff.php';
55
+*   $to_text = FineDiff::renderToTextFromOpcodes($from_text, $opcodes);
56
+*
57
+*   ...
58
+*/
59
+
60
+/**
61
+* Persisted opcodes (string) are a sequence of atomic opcode.
62
+* A single opcode can be one of the following:
63
+*   c | c{n} | d | d{n} | i:{c} | i{length}:{s}
64
+*   'c'        = copy one character from source
65
+*   'c{n}'     = copy n characters from source
66
+*   'd'        = skip one character from source
67
+*   'd{n}'     = skip n characters from source
68
+*   'i:{c}     = insert character 'c'
69
+*   'i{n}:{s}' = insert string s, which is of length n
70
+*
71
+* Do not exist as of now, under consideration:
72
+*   'm{n}:{o}  = move n characters from source o characters ahead.
73
+*   It would be essentially a shortcut for a delete->copy->insert
74
+*   command (swap) for when the inserted segment is exactly the same
75
+*   as the deleted one, and with only a copy operation in between.
76
+*   TODO: How often this case occurs? Is it worth it? Can only
77
+*   be done as a postprocessing method (->optimize()?)
78
+*/
79
+abstract class FineDiffOp {
80
+   abstract public function getFromLen();
81
+   abstract public function getToLen();
82
+   abstract public function getOpcode();
83
+   }
84
+
85
+class FineDiffDeleteOp extends FineDiffOp {
86
+   public function __construct($len) {
87
+       $this->fromLen = $len;
88
+       }
89
+   public function getFromLen() {
90
+       return $this->fromLen;
91
+       }
92
+   public function getToLen() {
93
+       return 0;
94
+       }
95
+   public function getOpcode() {
96
+       if ( $this->fromLen === 1 ) {
97
+           return 'd';
98
+           }
99
+       return "d{$this->fromLen}";
100
+       }
101
+   }
102
+
103
+class FineDiffInsertOp extends FineDiffOp {
104
+   public function __construct($text) {
105
+       $this->text = $text;
106
+       }
107
+   public function getFromLen() {
108
+       return 0;
109
+       }
110
+   public function getToLen() {
111
+       return strlen($this->text);
112
+       }
113
+   public function getText() {
114
+       return $this->text;
115
+       }
116
+   public function getOpcode() {
117
+       $to_len = strlen($this->text);
118
+       if ( $to_len === 1 ) {
119
+           return "i:{$this->text}";
120
+           }
121
+       return "i{$to_len}:{$this->text}";
122
+       }
123
+   }
124
+
125
+class FineDiffReplaceOp extends FineDiffOp {
126
+   public function __construct($fromLen, $text) {
127
+       $this->fromLen = $fromLen;
128
+       $this->text = $text;
129
+       }
130
+   public function getFromLen() {
131
+       return $this->fromLen;
132
+       }
133
+   public function getToLen() {
134
+       return strlen($this->text);
135
+       }
136
+   public function getText() {
137
+       return $this->text;
138
+       }
139
+   public function getOpcode() {
140
+       if ( $this->fromLen === 1 ) {
141
+           $del_opcode = 'd';
142
+           }
143
+       else {
144
+           $del_opcode = "d{$this->fromLen}";
145
+           }
146
+       $to_len = strlen($this->text);
147
+       if ( $to_len === 1 ) {
148
+           return "{$del_opcode}i:{$this->text}";
149
+           }
150
+       return "{$del_opcode}i{$to_len}:{$this->text}";
151
+       }
152
+   }
153
+
154
+class FineDiffCopyOp extends FineDiffOp {
155
+   public function __construct($len) {
156
+       $this->len = $len;
157
+       }
158
+   public function getFromLen() {
159
+       return $this->len;
160
+       }
161
+   public function getToLen() {
162
+       return $this->len;
163
+       }
164
+   public function getOpcode() {
165
+       if ( $this->len === 1 ) {
166
+           return 'c';
167
+           }
168
+       return "c{$this->len}";
169
+       }
170
+   public function increase($size) {
171
+       return $this->len += $size;
172
+       }
173
+   }
174
+
175
+/**
176
+* FineDiff ops
177
+*
178
+* Collection of ops
179
+*/
180
+class FineDiffOps {
181
+   public function appendOpcode($opcode, $from, $from_offset, $from_len) {
182
+       if ( $opcode === 'c' ) {
183
+           $edits[] = new FineDiffCopyOp($from_len);
184
+           }
185
+       else if ( $opcode === 'd' ) {
186
+           $edits[] = new FineDiffDeleteOp($from_len);
187
+           }
188
+       else /* if ( $opcode === 'i' ) */ {
189
+           $edits[] = new FineDiffInsertOp(substr($from, $from_offset, $from_len));
190
+           }
191
+       }
192
+   public $edits = array();
193
+   }
194
+
195
+/**
196
+* FineDiff class
197
+*
198
+* TODO: Document
199
+*
200
+*/
201
+class FineDiff {
202
+
203
+   /**------------------------------------------------------------------------
204
+   *
205
+   * Public section
206
+   *
207
+   */
208
+
209
+   /**
210
+   * Constructor
211
+   * ...
212
+   * The $granularityStack allows FineDiff to be configurable so that
213
+   * a particular stack tailored to the specific content of a document can
214
+   * be passed.
215
+   */
216
+   public function __construct($from_text = '', $to_text = '', $granularityStack = null) {
217
+       // setup stack for generic text documents by default
218
+       $this->granularityStack = $granularityStack ? $granularityStack : FineDiff::$characterGranularity;
219
+       $this->edits = array();
220
+       $this->from_text = $from_text;
221
+       $this->doDiff($from_text, $to_text);
222
+       }
223
+
224
+   public function getOps() {
225
+       return $this->edits;
226
+       }
227
+
228
+   public function getOpcodes() {
229
+       $opcodes = array();
230
+       foreach ( $this->edits as $edit ) {
231
+           $opcodes[] = $edit->getOpcode();
232
+           }
233
+       return implode('', $opcodes);
234
+       }
235
+
236
+   public function renderDiffToHTML() {
237
+       $in_offset = 0;
238
+       $html = '';
239
+       foreach ( $this->edits as $edit ) {
240
+           $n = $edit->getFromLen();
241
+           if ( $edit instanceof FineDiffCopyOp ) {
242
+               $html .= FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
243
+               }
244
+           else if ( $edit instanceof FineDiffDeleteOp ) {
245
+               $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
246
+               }
247
+           else if ( $edit instanceof FineDiffInsertOp ) {
248
+               $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
249
+               }
250
+           else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
251
+               $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
252
+               $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
253
+               }
254
+           $in_offset += $n;
255
+           }
256
+       return $html;
257
+       }
258
+
259
+   /**------------------------------------------------------------------------
260
+   * Return an opcodes string describing the diff between a "From" and a
261
+   * "To" string
262
+   */
263
+   public static function getDiffOpcodes($from, $to, $granularities = null) {
264
+       $diff = new FineDiff($from, $to, $granularities);
265
+       return $diff->getOpcodes();
266
+       }
267
+
268
+   /**------------------------------------------------------------------------
269
+   * Return an iterable collection of diff ops from an opcodes string
270
+   */
271
+   public static function getDiffOpsFromOpcodes($opcodes) {
272
+       $diffops = new FineDiffOps();
273
+       FineDiff::renderFromOpcodes(null, $opcodes, array($diffops,'appendOpcode'));
274
+       return $diffops->edits;
275
+       }
276
+
277
+   /**------------------------------------------------------------------------
278
+   * Re-create the "To" string from the "From" string and an "Opcodes" string
279
+   */
280
+   public static function renderToTextFromOpcodes($from, $opcodes) {
281
+       return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
282
+       }
283
+
284
+   /**------------------------------------------------------------------------
285
+   * Render the diff to an HTML string
286
+   */
287
+   public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
288
+       return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
289
+       }
290
+
291
+   /**------------------------------------------------------------------------
292
+   * Generic opcodes parser, user must supply callback for handling
293
+   * single opcode
294
+   */
295
+   public static function renderFromOpcodes($from, $opcodes, $callback) {
296
+       if ( !is_callable($callback) ) {
297
+           return '';
298
+           }
299
+       $out = '';
300
+       $opcodes_len = strlen($opcodes);
301
+       $from_offset = $opcodes_offset = 0;
302
+       while ( $opcodes_offset <  $opcodes_len ) {
303
+           $opcode = substr($opcodes, $opcodes_offset, 1);
304
+           $opcodes_offset++;
305
+           $n = intval(substr($opcodes, $opcodes_offset));
306
+           if ( $n ) {
307
+               $opcodes_offset += strlen(strval($n));
308
+               }
309
+           else {
310
+               $n = 1;
311
+               }
312
+           if ( $opcode === 'c' ) { // copy n characters from source
313
+               $out .= call_user_func($callback, 'c', $from, $from_offset, $n, '');
314
+               $from_offset += $n;
315
+               }
316
+           else if ( $opcode === 'd' ) { // delete n characters from source
317
+               $out .= call_user_func($callback, 'd', $from, $from_offset, $n, '');
318
+               $from_offset += $n;
319
+               }
320
+           else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
321
+               $out .= call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
322
+               $opcodes_offset += 1 + $n;
323
+               }
324
+           }
325
+       return $out;
326
+       }
327
+
328
+   /**
329
+   * Stock granularity stacks and delimiters
330
+   */
331
+
332
+   const paragraphDelimiters = "\n\r";
333
+   public static $paragraphGranularity = array(
334
+       FineDiff::paragraphDelimiters
335
+       );
336
+   const sentenceDelimiters = ".\n\r";
337
+   public static $sentenceGranularity = array(
338
+       FineDiff::paragraphDelimiters,
339
+       FineDiff::sentenceDelimiters
340
+       );
341
+   const wordDelimiters = " \t.\n\r";
342
+   public static $wordGranularity = array(
343
+       FineDiff::paragraphDelimiters,
344
+       FineDiff::sentenceDelimiters,
345
+       FineDiff::wordDelimiters
346
+       );
347
+   const characterDelimiters = "";
348
+   public static $characterGranularity = array(
349
+       FineDiff::paragraphDelimiters,
350
+       FineDiff::sentenceDelimiters,
351
+       FineDiff::wordDelimiters,
352
+       FineDiff::characterDelimiters
353
+       );
354
+
355
+   public static $textStack = array(
356
+       ".",
357
+       " \t.\n\r",
358
+       ""
359
+       );
360
+
361
+   /**------------------------------------------------------------------------
362
+   *
363
+   * Private section
364
+   *
365
+   */
366
+
367
+   /**
368
+   * Entry point to compute the diff.
369
+   */
370
+   private function doDiff($from_text, $to_text) {
371
+       $this->last_edit = false;
372
+       $this->stackpointer = 0;
373
+       $this->from_text = $from_text;
374
+       $this->from_offset = 0;
375
+       // can't diff without at least one granularity specifier
376
+       if ( empty($this->granularityStack) ) {
377
+           return;
378
+           }
379
+       $this->_processGranularity($from_text, $to_text);
380
+       }
381
+
382
+   /**
383
+   * This is the recursive function which is responsible for
384
+   * handling/increasing granularity.
385
+   *
386
+   * Incrementally increasing the granularity is key to compute the
387
+   * overall diff in a very efficient way.
388
+   */
389
+   private function _processGranularity($from_segment, $to_segment) {
390
+       $delimiters = $this->granularityStack[$this->stackpointer++];
391
+       $has_next_stage = $this->stackpointer < count($this->granularityStack);
392
+       foreach ( FineDiff::doFragmentDiff($from_segment, $to_segment, $delimiters) as $fragment_edit ) {
393
+           // increase granularity
394
+           if ( $fragment_edit instanceof FineDiffReplaceOp && $has_next_stage ) {
395
+               $this->_processGranularity(
396
+                   substr($this->from_text, $this->from_offset, $fragment_edit->getFromLen()),
397
+                   $fragment_edit->getText()
398
+                   );
399
+               }
400
+           // fuse copy ops whenever possible
401
+           else if ( $fragment_edit instanceof FineDiffCopyOp && $this->last_edit instanceof FineDiffCopyOp ) {
402
+               $this->edits[count($this->edits)-1]->increase($fragment_edit->getFromLen());
403
+               $this->from_offset += $fragment_edit->getFromLen();
404
+               }
405
+           else {
406
+               /* $fragment_edit instanceof FineDiffCopyOp */
407
+               /* $fragment_edit instanceof FineDiffDeleteOp */
408
+               /* $fragment_edit instanceof FineDiffInsertOp */
409
+               $this->edits[] = $this->last_edit = $fragment_edit;
410
+               $this->from_offset += $fragment_edit->getFromLen();
411
+               }
412
+           }
413
+       $this->stackpointer--;
414
+       }
415
+
416
+   /**
417
+   * This is the core algorithm which actually perform the diff itself,
418
+   * fragmenting the strings as per specified delimiters.
419
+   *
420
+   * This function is naturally recursive, however for performance purpose
421
+   * a local job queue is used instead of outright recursivity.
422
+   */
423
+   private static function doFragmentDiff($from_text, $to_text, $delimiters) {
424
+       // Empty delimiter means character-level diffing.
425
+       // In such case, use code path optimized for character-level
426
+       // diffing.
427
+       if ( empty($delimiters) ) {
428
+           return FineDiff::doCharDiff($from_text, $to_text);
429
+           }
430
+
431
+       $result = array();
432
+
433
+       // fragment-level diffing
434
+       $from_text_len = strlen($from_text);
435
+       $to_text_len = strlen($to_text);
436
+       $from_fragments = FineDiff::extractFragments($from_text, $delimiters);
437
+       $to_fragments = FineDiff::extractFragments($to_text, $delimiters);
438
+
439
+       $jobs = array(array(0, $from_text_len, 0, $to_text_len));
440
+
441
+       $cached_array_keys = array();
442
+
443
+       while ( $job = array_pop($jobs) ) {
444
+
445
+           // get the segments which must be diff'ed
446
+           list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
447
+
448
+           // catch easy cases first
449
+           $from_segment_length = $from_segment_end - $from_segment_start;
450
+           $to_segment_length = $to_segment_end - $to_segment_start;
451
+           if ( !$from_segment_length || !$to_segment_length ) {
452
+               if ( $from_segment_length ) {
453
+                   $result[$from_segment_start * 4] = new FineDiffDeleteOp($from_segment_length);
454
+                   }
455
+               else if ( $to_segment_length ) {
456
+                   $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_length));
457
+                   }
458
+               continue;
459
+               }
460
+
461
+           // find longest copy operation for the current segments
462
+           $best_copy_length = 0;
463
+
464
+           $from_base_fragment_index = $from_segment_start;
465
+
466
+           $cached_array_keys_for_current_segment = array();
467
+
468
+           while ( $from_base_fragment_index < $from_segment_end ) {
469
+               $from_base_fragment = $from_fragments[$from_base_fragment_index];
470
+               $from_base_fragment_length = strlen($from_base_fragment);
471
+               // performance boost: cache array keys
472
+               if ( !isset($cached_array_keys_for_current_segment[$from_base_fragment]) ) {
473
+                   if ( !isset($cached_array_keys[$from_base_fragment]) ) {
474
+                       $to_all_fragment_indices = $cached_array_keys[$from_base_fragment] = array_keys($to_fragments, $from_base_fragment, true);
475
+                       }
476
+                   else {
477
+                       $to_all_fragment_indices = $cached_array_keys[$from_base_fragment];
478
+                       }
479
+                   // get only indices which falls within current segment
480
+                   if ( $to_segment_start > 0 || $to_segment_end < $to_text_len ) {
481
+                       $to_fragment_indices = array();
482
+                       foreach ( $to_all_fragment_indices as $to_fragment_index ) {
483
+                           if ( $to_fragment_index < $to_segment_start ) { continue; }
484
+                           if ( $to_fragment_index >= $to_segment_end ) { break; }
485
+                           $to_fragment_indices[] = $to_fragment_index;
486
+                           }
487
+                       $cached_array_keys_for_current_segment[$from_base_fragment] = $to_fragment_indices;
488
+                       }
489
+                   else {
490
+                       $to_fragment_indices = $to_all_fragment_indices;
491
+                       }
492
+                   }
493
+               else {
494
+                   $to_fragment_indices = $cached_array_keys_for_current_segment[$from_base_fragment];
495
+                   }
496
+               // iterate through collected indices
497
+               foreach ( $to_fragment_indices as $to_base_fragment_index ) {
498
+                   $fragment_index_offset = $from_base_fragment_length;
499
+                   // iterate until no more match
500
+                   for (;;) {
501
+                       $fragment_from_index = $from_base_fragment_index + $fragment_index_offset;
502
+                       if ( $fragment_from_index >= $from_segment_end ) {
503
+                           break;
504
+                           }
505
+                       $fragment_to_index = $to_base_fragment_index + $fragment_index_offset;
506
+                       if ( $fragment_to_index >= $to_segment_end ) {
507
+                           break;
508
+                           }
509
+                       if ( $from_fragments[$fragment_from_index] !== $to_fragments[$fragment_to_index] ) {
510
+                           break;
511
+                           }
512
+                       $fragment_length = strlen($from_fragments[$fragment_from_index]);
513
+                       $fragment_index_offset += $fragment_length;
514
+                       }
515
+                   if ( $fragment_index_offset > $best_copy_length ) {
516
+                       $best_copy_length = $fragment_index_offset;
517
+                       $best_from_start = $from_base_fragment_index;
518
+                       $best_to_start = $to_base_fragment_index;
519
+                       }
520
+                   }
521
+               $from_base_fragment_index += strlen($from_base_fragment);
522
+               // If match is larger than half segment size, no point trying to find better
523
+               // TODO: Really?
524
+               if ( $best_copy_length >= $from_segment_length / 2) {
525
+                   break;
526
+                   }
527
+               // no point to keep looking if what is left is less than
528
+               // current best match
529
+               if ( $from_base_fragment_index + $best_copy_length >= $from_segment_end ) {
530
+                   break;
531
+                   }
532
+               }
533
+
534
+           if ( $best_copy_length ) {
535
+               $jobs[] = array($from_segment_start, $best_from_start, $to_segment_start, $best_to_start);
536
+               $result[$best_from_start * 4 + 2] = new FineDiffCopyOp($best_copy_length);
537
+               $jobs[] = array($best_from_start + $best_copy_length, $from_segment_end, $best_to_start + $best_copy_length, $to_segment_end);
538
+               }
539
+           else {
540
+               $result[$from_segment_start * 4 ] = new FineDiffReplaceOp($from_segment_length, substr($to_text, $to_segment_start, $to_segment_length));
541
+               }
542
+           }
543
+
544
+       ksort($result, SORT_NUMERIC);
545
+       return array_values($result);
546
+       }
547
+
548
+   /**
549
+   * Perform a character-level diff.
550
+   *
551
+   * The algorithm is quite similar to doFragmentDiff(), except that
552
+   * the code path is optimized for character-level diff -- strpos() is
553
+   * used to find out the longest common subequence of characters.
554
+   *
555
+   * We try to find a match using the longest possible subsequence, which
556
+   * is at most the length of the shortest of the two strings, then incrementally
557
+   * reduce the size until a match is found.
558
+   *
559
+   * I still need to study more the performance of this function. It
560
+   * appears that for long strings, the generic doFragmentDiff() is more
561
+   * performant. For word-sized strings, doCharDiff() is somewhat more
562
+   * performant.
563
+   */
564
+   private static function doCharDiff($from_text, $to_text) {
565
+       $result = array();
566
+       $jobs = array(array(0, strlen($from_text), 0, strlen($to_text)));
567
+       while ( $job = array_pop($jobs) ) {
568
+           // get the segments which must be diff'ed
569
+           list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
570
+           $from_segment_len = $from_segment_end - $from_segment_start;
571
+           $to_segment_len = $to_segment_end - $to_segment_start;
572
+
573
+           // catch easy cases first
574
+           if ( !$from_segment_len || !$to_segment_len ) {
575
+               if ( $from_segment_len ) {
576
+                   $result[$from_segment_start * 4 + 0] = new FineDiffDeleteOp($from_segment_len);
577
+                   }
578
+               else if ( $to_segment_len ) {
579
+                   $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_len));
580
+                   }
581
+               continue;
582
+               }
583
+           if ( $from_segment_len >= $to_segment_len ) {
584
+               $copy_len = $to_segment_len;
585
+               while ( $copy_len ) {
586
+                   $to_copy_start = $to_segment_start;
587
+                   $to_copy_start_max = $to_segment_end - $copy_len;
588
+                   while ( $to_copy_start <= $to_copy_start_max ) {
589
+                       $from_copy_start = strpos(substr($from_text, $from_segment_start, $from_segment_len), substr($to_text, $to_copy_start, $copy_len));
590
+                       if ( $from_copy_start !== false ) {
591
+                           $from_copy_start += $from_segment_start;
592
+                           break 2;
593
+                           }
594
+                       $to_copy_start++;
595
+                       }
596
+                   $copy_len--;
597
+                   }
598
+               }
599
+           else {
600
+               $copy_len = $from_segment_len;
601
+               while ( $copy_len ) {
602
+                   $from_copy_start = $from_segment_start;
603
+                   $from_copy_start_max = $from_segment_end - $copy_len;
604
+                   while ( $from_copy_start <= $from_copy_start_max ) {
605
+                       $to_copy_start = strpos(substr($to_text, $to_segment_start, $to_segment_len), substr($from_text, $from_copy_start, $copy_len));
606
+                       if ( $to_copy_start !== false ) {
607
+                           $to_copy_start += $to_segment_start;
608
+                           break 2;
609
+                           }
610
+                       $from_copy_start++;
611
+                       }
612
+                   $copy_len--;
613
+                   }
614
+               }
615
+           // match found
616
+           if ( $copy_len ) {
617
+               $jobs[] = array($from_segment_start, $from_copy_start, $to_segment_start, $to_copy_start);
618
+               $result[$from_copy_start * 4 + 2] = new FineDiffCopyOp($copy_len);
619
+               $jobs[] = array($from_copy_start + $copy_len, $from_segment_end, $to_copy_start + $copy_len, $to_segment_end);
620
+               }
621
+           // no match,  so delete all, insert all
622
+           else {
623
+               $result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_len, substr($to_text, $to_segment_start, $to_segment_len));
624
+               }
625
+           }
626
+       ksort($result, SORT_NUMERIC);
627
+       return array_values($result);
628
+       }
629
+
630
+   /**
631
+   * Efficiently fragment the text into an array according to
632
+   * specified delimiters.
633
+   * No delimiters means fragment into single character.
634
+   * The array indices are the offset of the fragments into
635
+   * the input string.
636
+   * A sentinel empty fragment is always added at the end.
637
+   * Careful: No check is performed as to the validity of the
638
+   * delimiters.
639
+   */
640
+   private static function extractFragments($text, $delimiters) {
641
+       // special case: split into characters
642
+       if ( empty($delimiters) ) {
643
+           $chars = str_split($text, 1);
644
+           $chars[strlen($text)] = '';
645
+           return $chars;
646
+           }
647
+       $fragments = array();
648
+       $start = $end = 0;
649
+       for (;;) {
650
+           $end += strcspn($text, $delimiters, $end);
651
+           $end += strspn($text, $delimiters, $end);
652
+           if ( $end === $start ) {
653
+               break;
654
+               }
655
+           $fragments[$start] = substr($text, $start, $end - $start);
656
+           $start = $end;
657
+           }
658
+       $fragments[$start] = '';
659
+       return $fragments;
660
+       }
661
+
662
+   /**
663
+   * Stock opcode renderers
664
+   */
665
+   private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
666
+       if ( $opcode === 'c' || $opcode === 'i' ) {
667
+           return substr($from, $from_offset, $from_len);
668
+           }
669
+       return '';
670
+       }
671
+
672
+   private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
673
+       if ( $opcode === 'c' ) {
674
+           return htmlentities(substr($from, $from_offset, $from_len));
675
+           }
676
+       else if ( $opcode === 'd' ) {
677
+           $deletion = substr($from, $from_offset, $from_len);
678
+           if ( strcspn($deletion, " \n\r") === 0 ) {
679
+               $deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
680
+               }
681
+           return '<del>' . htmlentities($deletion) . '</del>';
682
+           }
683
+       else /* if ( $opcode === 'i' ) */ {
684
+           return '<ins>' . htmlentities(substr($from, $from_offset, $from_len)) . '</ins>';
685
+           }
686
+       return '';
687
+       }
688
+   }
689
+
690
iRony-0.4.4.tar.gz/lib/FileAPI/drivers/kolab/plugins/libkolab/vendor/finediff_modifications.diff Added
123
 
1
@@ -0,0 +1,121 @@
2
+--- finediff.php.orig  2014-07-29 14:24:10.000000000 +0200
3
++++ finediff.php   2014-07-29 14:30:38.000000000 +0200
4
+@@ -234,25 +234,25 @@
5
+ 
6
+   public function renderDiffToHTML() {
7
+       $in_offset = 0;
8
+-      ob_start();
9
++      $html = '';
10
+       foreach ( $this->edits as $edit ) {
11
+           $n = $edit->getFromLen();
12
+           if ( $edit instanceof FineDiffCopyOp ) {
13
+-              FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
14
++              $html .= FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
15
+               }
16
+           else if ( $edit instanceof FineDiffDeleteOp ) {
17
+-              FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
18
++              $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
19
+               }
20
+           else if ( $edit instanceof FineDiffInsertOp ) {
21
+-              FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
22
++              $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
23
+               }
24
+           else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
25
+-              FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
26
+-              FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
27
++              $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
28
++              $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
29
+               }
30
+           $in_offset += $n;
31
+           }
32
+-      return ob_get_clean();
33
++      return $html;
34
+       }
35
+ 
36
+   /**------------------------------------------------------------------------
37
+@@ -277,18 +277,14 @@
38
+   * Re-create the "To" string from the "From" string and an "Opcodes" string
39
+   */
40
+   public static function renderToTextFromOpcodes($from, $opcodes) {
41
+-      ob_start();
42
+-      FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
43
+-      return ob_get_clean();
44
++      return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
45
+       }
46
+ 
47
+   /**------------------------------------------------------------------------
48
+   * Render the diff to an HTML string
49
+   */
50
+   public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
51
+-      ob_start();
52
+-      FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
53
+-      return ob_get_clean();
54
++      return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
55
+       }
56
+ 
57
+   /**------------------------------------------------------------------------
58
+@@ -297,8 +293,9 @@
59
+   */
60
+   public static function renderFromOpcodes($from, $opcodes, $callback) {
61
+       if ( !is_callable($callback) ) {
62
+-          return;
63
++          return '';
64
+           }
65
++      $out = '';
66
+       $opcodes_len = strlen($opcodes);
67
+       $from_offset = $opcodes_offset = 0;
68
+       while ( $opcodes_offset <  $opcodes_len ) {
69
+@@ -312,18 +309,19 @@
70
+               $n = 1;
71
+               }
72
+           if ( $opcode === 'c' ) { // copy n characters from source
73
+-              call_user_func($callback, 'c', $from, $from_offset, $n, '');
74
++              $out .= call_user_func($callback, 'c', $from, $from_offset, $n, '');
75
+               $from_offset += $n;
76
+               }
77
+           else if ( $opcode === 'd' ) { // delete n characters from source
78
+-              call_user_func($callback, 'd', $from, $from_offset, $n, '');
79
++              $out .= call_user_func($callback, 'd', $from, $from_offset, $n, '');
80
+               $from_offset += $n;
81
+               }
82
+           else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
83
+-              call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
84
++              $out .= call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
85
+               $opcodes_offset += 1 + $n;
86
+               }
87
+           }
88
++      return $out;
89
+       }
90
+ 
91
+   /**
92
+@@ -665,24 +663,26 @@
93
+   */
94
+   private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
95
+       if ( $opcode === 'c' || $opcode === 'i' ) {
96
+-          echo substr($from, $from_offset, $from_len);
97
++          return substr($from, $from_offset, $from_len);
98
+           }
99
++      return '';
100
+       }
101
+ 
102
+   private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
103
+       if ( $opcode === 'c' ) {
104
+-          echo htmlentities(substr($from, $from_offset, $from_len));
105
++          return htmlentities(substr($from, $from_offset, $from_len));
106
+           }
107
+       else if ( $opcode === 'd' ) {
108
+           $deletion = substr($from, $from_offset, $from_len);
109
+           if ( strcspn($deletion, " \n\r") === 0 ) {
110
+               $deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
111
+               }
112
+-          echo '<del>', htmlentities($deletion), '</del>';
113
++          return '<del>' . htmlentities($deletion) . '</del>';
114
+           }
115
+       else /* if ( $opcode === 'i' ) */ {
116
+-          echo '<ins>', htmlentities(substr($from, $from_offset, $from_len)), '</ins>';
117
++          return '<ins>' . htmlentities(substr($from, $from_offset, $from_len)) . '</ins>';
118
+           }
119
++      return '';
120
+       }
121
+   }
122
+ 
123
iRony-0.4.3.tar.gz/lib/FileAPI/drivers/seafile/seafile_api.php -> iRony-0.4.4.tar.gz/lib/FileAPI/drivers/seafile/seafile_api.php Changed
657
 
1
@@ -77,31 +77,16 @@
2
      */
3
     protected $token;
4
 
5
-    /**
6
-     * API URL prefix (schema and host[:port])
7
-     *
8
-     * @var string
9
-     */
10
-    protected $url_prefix;
11
-
12
 
13
     public function __construct($config = array())
14
     {
15
         $this->config = $config;
16
-        $this->token  = $config['token'];
17
 
18
         // set Web API URI
19
-        $this->url = rtrim(trim($config['host']), '/') ?: 'localhost';
20
-
21
-        if (!preg_match('|^https?://|i', $this->url)) {
22
-            $this->url = 'https://' . $this->url;
23
-        }
24
-
25
+        $this->url = rtrim('https://' . ($config['host'] ?: 'localhost'), '/');
26
         if (!preg_match('|/api2$|', $this->url)) {
27
             $this->url .= '/api2/';
28
         }
29
-
30
-        $this->url_prefix = preg_replace('|^(https?://[^/]+).*$|i', '\\1', $this->url);
31
     }
32
 
33
     /**
34
@@ -124,10 +109,6 @@
35
             'ssl_cafile', 'ssl_capath', 'ssl_local_cert', 'ssl_passphrase'
36
         )));
37
 
38
-        // force CURL adapter, this allows to handle correctly
39
-        // compressed responses with simple SplObserver registered
40
-        $config['adapter'] = 'HTTP_Request2_Adapter_Curl';
41
-
42
         try {
43
             $request = new HTTP_Request2();
44
             $request->setConfig($config);
45
@@ -143,29 +124,21 @@
46
     /**
47
      * Send HTTP request
48
      *
49
-     * @param string $method  Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT')
50
-     * @param string $url     Request API URL
51
-     * @param array  $get     GET parameters
52
-     * @param array  $post    POST parameters
53
-     * @param array  $upload  Uploaded files data
54
-     * @param string $version API version (to replace "api2" with "api/v$version" in the URL
55
+     * @param string $method Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT')
56
+     * @param string $url    Request API URL
57
+     * @param array  $get    GET parameters
58
+     * @param array  $post   POST parameters
59
+     * @param array  $upload Uploaded files data
60
      *
61
      * @return string|array Server response
62
      */
63
-    protected function request($method, $url, $get = null, $post = null, $upload = null, $version = null)
64
+    protected function request($method, $url, $get = null, $post = null, $upload = null)
65
     {
66
         if (!preg_match('/^https?:\/\//', $url)) {
67
             $url = $this->url . $url;
68
             // Note: It didn't work for me without the last backslash
69
             $url = rtrim($url, '/') . '/';
70
         }
71
-        else {
72
-            $url = $this->mod_url($url);
73
-        }
74
-
75
-        if ($version && $version != 2) {
76
-            $url = str_replace('/api2/', "/api/v$version/", $url);
77
-        }
78
 
79
         if (!$this->request) {
80
             $this->config['store_body']       = true;
81
@@ -219,16 +192,6 @@
82
             $this->request->setUrl($_url);
83
         }
84
 
85
-        // HTTP_Request2 does not support POST params on PUT requests
86
-        if (!empty($post) && $method == 'PUT') {
87
-            $body = http_build_query($post, '', '&');
88
-            $body = str_replace('%7E', '~', $body); // support RFC 3986 by not encoding '~' symbol
89
-            $post = null;
90
-
91
-            $this->request->setBody($body);
92
-            $this->request->setHeader('content-type', 'application/x-www-form-urlencoded');
93
-        }
94
-
95
         if (!empty($post)) {
96
             $this->request->addPostParameter($post);
97
         }
98
@@ -272,22 +235,8 @@
99
             }
100
         }
101
 
102
-        if ($this->status < 400) {
103
-            $body = @json_decode($body, true);
104
-        }
105
-
106
-        // Sometimes we can get an error with response code 200
107
-        if ($this->status == 200
108
-            && ($method == 'PUT' || $method == 'POST')
109
-            && !empty($body) && is_array($body) && !empty($body['failed']) && empty($body['success'])
110
-        ) {
111
-            // Note: $body['failed'] is an array where each element is an array with 'error_msg' item
112
-            // TODO: Support partial success/error result
113
-            $this->status = 520;
114
-        }
115
-
116
         // decode response
117
-        return $this->status >= 400 ? false : $body;
118
+        return $this->status >= 400 ? false : @json_decode($body, true);
119
     }
120
 
121
     /**
122
@@ -426,14 +375,12 @@
123
     /**
124
      * List directory entries (files and directories)
125
      *
126
-     * @param string $repo_id   Library identifier
127
-     * @param string $dir       Directory name (with path)
128
-     * @param string $type      Entry type ('dir' or 'file') (requires Seafile 4.4.1)
129
-     * @param bool   $recursive Enable recursive call for 'dir' listing (requires Seafile 4.4.1)
130
+     * @param string $repo_id Library identifier
131
+     * @param string $dir     Directory name (with path)
132
      *
133
      * @return bool|array List of directories/files on success, False on failure
134
      */
135
-    public function directory_entries($repo_id, $dir, $type = null, $recursive = false)
136
+    public function directory_entries($repo_id, $dir)
137
     {
138
         // sanity checks
139
         if (!is_string($dir)) {
140
@@ -463,43 +410,7 @@
141
         //    "name": "test_dir"
142
         // }]
143
 
144
-        $params = array('p' => $dir);
145
-
146
-        if ($type) {
147
-            $params['t'] = $type == 'dir' ? 'd' : 'f';
148
-        }
149
-
150
-        if ($recursive && $type == 'dir') {
151
-            $params['recursive'] = 1;
152
-        }
153
-
154
-        return $this->request('GET', "repos/$repo_id/dir", $params);
155
-    }
156
-
157
-    /**
158
-     * Get directory information.
159
-     *
160
-     * @param string $repo_id Library identifier
161
-     * @param string $dir     Directory name (with path)
162
-     *
163
-     * @return bool|array Directory properties on success, False on failure
164
-     */
165
-    public function directory_info($repo_id, $dir)
166
-    {
167
-        // sanity checks
168
-        if (!is_string($dir)) {
169
-            $this->status = self::BAD_REQUEST;
170
-            return false;
171
-        }
172
-
173
-        if ($repo_id === '' || !is_string($repo_id)) {
174
-            $this->status = self::BAD_REQUEST;
175
-            return false;
176
-        }
177
-
178
-        $params = array('path' => $dir);
179
-
180
-        return $this->request('GET', "repos/$repo_id/dir/detail", $params, null, null, '2.1');
181
+        return $this->request('GET', "repos/$repo_id/dir", array('p' => $dir));
182
     }
183
 
184
     /**
185
@@ -935,471 +846,4 @@
186
 
187
         return $this->is_error() === false;
188
     }
189
-
190
-    /**
191
-     * Share a directory (or library)
192
-     *
193
-     * @param string $repo_id Library identifier
194
-     * @param string $dir     Directory name (with path)
195
-     * @param string $right   Permission ('r' or 'rw' or 'admin')
196
-     * @param string $mode    Mode ('user' or 'group')
197
-     * @param string $who     Username or Group ID
198
-     * @param bool   $update  Update an existing entry
199
-     *
200
-     * @return bool True on success, False on failure
201
-     */
202
-    public function shared_item_add($repo_id, $dir, $right, $mode, $who, $update = false)
203
-    {
204
-        // sanity checks
205
-        if (!is_string($dir)) {
206
-            $this->status = self::BAD_REQUEST;
207
-            return false;
208
-        }
209
-
210
-        if ($repo_id === '' || !is_string($repo_id)) {
211
-            $this->status = self::BAD_REQUEST;
212
-            return false;
213
-        }
214
-
215
-        if ($mode != 'user' && $mode != 'group') {
216
-            $this->status = self::BAD_REQUEST;
217
-            return false;
218
-        }
219
-
220
-        if ($right != 'r' && $right != 'rw' && $right != 'admin') {
221
-            $this->status = self::BAD_REQUEST;
222
-            return false;
223
-        }
224
-
225
-        if ($dir === '') {
226
-            $dir = '/';
227
-        }
228
-
229
-        $post = array(
230
-            'permission' => $right,
231
-            'share_type' => $mode,
232
-        );
233
-
234
-        $post[$mode == 'group' ? 'group_id' : 'username'] = $who;
235
-
236
-        $this->request($update ? 'POST' : 'PUT', "repos/$repo_id/dir/shared_items", array('p' => $dir), $post);
237
-
238
-        return $this->is_error() === false;
239
-    }
240
-
241
-    /**
242
-     * Update shared item permissions
243
-     *
244
-     * @param string $repo_id Library identifier
245
-     * @param string $dir     Directory name (with path)
246
-     * @param string $right   Permission ('r' or 'rw' or 'admin')
247
-     * @param string $mode    Mode ('user' or 'group')
248
-     * @param string $who     Username or Group ID
249
-     *
250
-     * @return bool True on success, False on failure
251
-     */
252
-    public function shared_item_update($repo_id, $dir, $right, $mode, $who)
253
-    {
254
-        return $this->shared_item_add($repo_id, $dir, $right, $mode, $who, true);
255
-    }
256
-
257
-    /**
258
-     * Un-Share a directory (or library)
259
-     *
260
-     * @param string $repo_id Library identifier
261
-     * @param string $dir     Directory name (with path)
262
-     * @param string $mode    Mode ('user' or 'group')
263
-     * @param string $who     Username or Group ID
264
-     *
265
-     * @return bool True on success, False on failure
266
-     */
267
-    public function shared_item_delete($repo_id, $dir, $mode, $who)
268
-    {
269
-        // sanity checks
270
-        if (!is_string($dir)) {
271
-            $this->status = self::BAD_REQUEST;
272
-            return false;
273
-        }
274
-
275
-        if ($repo_id === '' || !is_string($repo_id)) {
276
-            $this->status = self::BAD_REQUEST;
277
-            return false;
278
-        }
279
-
280
-        if ($mode != 'user' && $mode != 'group') {
281
-            $this->status = self::BAD_REQUEST;
282
-            return false;
283
-        }
284
-
285
-        if ($dir === '') {
286
-            $dir = '/';
287
-        }
288
-
289
-        $get = array(
290
-            'share_type' => $mode,
291
-            'p'          => $dir
292
-        );
293
-
294
-        $get[$mode == 'group' ? 'group_id' : 'username'] = $who;
295
-
296
-        $this->request('DELETE', "repos/$repo_id/dir/shared_items", $get);
297
-
298
-        return $this->is_error() === false;
299
-    }
300
-
301
-    /**
302
-     * List directory permissions (shares)
303
-     *
304
-     * @param string $repo_id   Library identifier
305
-     * @param string $dir       Directory name (with path)
306
-     *
307
-     * @return bool|array List of user/group info on success, False on failure
308
-     */
309
-    public function shared_item_list($repo_id, $dir)
310
-    {
311
-        // sanity checks
312
-        if (!is_string($dir)) {
313
-            $this->status = self::BAD_REQUEST;
314
-            return false;
315
-        }
316
-
317
-        if ($repo_id === '' || !is_string($repo_id)) {
318
-            $this->status = self::BAD_REQUEST;
319
-            return false;
320
-        }
321
-
322
-        if ($dir === '') {
323
-            $dir = '/';
324
-        }
325
-
326
-        // Example result:
327
-        // [
328
-        //   {
329
-        //     "group_info": { "id": 17, "name": "Group Name" },
330
-        //     "is_admin": false,
331
-        //     "share_type": "group",
332
-        //     "permission": "rw"
333
-        //   },
334
-        //   {
335
-        //     "user_info": { "nickname": "user", "name": "user@domain.com" },
336
-        //     "share_type": "user",
337
-        //     "permission": "r"
338
-        //   }
339
-        // ]
340
-
341
-        return $this->request('GET', "repos/$repo_id/dir/shared_items", array('p' => $dir));
342
-    }
343
-
344
-    /**
345
-     * List share (download) links
346
-     *
347
-     * @param string $repo_id   Library identifier
348
-     * @param string $dir       Directory name (with path)
349
-     *
350
-     * @return bool|array List of shared links on success, False on failure
351
-     */
352
-    public function share_link_list($repo_id, $dir)
353
-    {
354
-        // sanity checks
355
-        if (!is_string($dir)) {
356
-            $this->status = self::BAD_REQUEST;
357
-            return false;
358
-        }
359
-
360
-        if ($repo_id === '' || !is_string($repo_id)) {
361
-            $this->status = self::BAD_REQUEST;
362
-            return false;
363
-        }
364
-
365
-        if ($dir === '') {
366
-            $dir = '/';
367
-        }
368
-
369
-        // Example result:
370
-        // [
371
-        //   {
372
-        //     "username": "lian@lian.com",
373
-        //     "repo_id": "104f6537-b3a5-4d42-b8b5-8e47e494e4cf",
374
-        //     "ctime": "2017-04-01T02:35:29+00:00",
375
-        //     "expire_date": "",
376
-        //     "token": "0c4eb0cb104a43caaeef",
377
-        //     "view_cnt": 0,
378
-        //     "link": "https://cloud.seafile.com/d/0c4eb0cb104a43caaeef/",
379
-        //     "obj_name": "folder",
380
-        //     "path": "/folder/",
381
-        //     "is_dir": true,
382
-        //     "is_expired": false,
383
-        //     "repo_name": "for-test-web-api"
384
-        //   }
385
-        // ]
386
-
387
-        return $this->request('GET', "share-links", array('repo_id' => $repo_id, 'path' => $dir), null, null, '2.1');
388
-    }
389
-
390
-    /**
391
-     * Create a shared (download) link
392
-     *
393
-     * @param string $repo_id  Library identifier
394
-     * @param string $dir      Directory name (with path)
395
-     * @param string $password Password
396
-     * @param string $expire   Days to expire
397
-     *
398
-     * @return bool Link info on success, False on failure
399
-     */
400
-    public function share_link_add($repo_id, $dir, $password = '', $expire = 0)
401
-    {
402
-        // sanity checks
403
-        if (!is_string($dir)) {
404
-            $this->status = self::BAD_REQUEST;
405
-            return false;
406
-        }
407
-
408
-        if ($repo_id === '' || !is_string($repo_id)) {
409
-            $this->status = self::BAD_REQUEST;
410
-            return false;
411
-        }
412
-
413
-        if (!empty($expire) && !is_numeric($expire)) {
414
-            $this->status = self::BAD_REQUEST;
415
-            return false;
416
-        }
417
-
418
-        if ($dir === '') {
419
-            $dir = '/';
420
-        }
421
-
422
-        $post = array(
423
-            'repo_id'     => $repo_id,
424
-            'path'        => $dir,
425
-        );
426
-
427
-        if (strlen($password)) {
428
-            $post['password'] = $password;
429
-        }
430
-        if ($expire > 0) {
431
-            $post['expire_days'] = $expire;
432
-        }
433
-
434
-        $result = $this->request('POST', "share-links", array(), $post, null, '2.1');
435
-
436
-        // Sample response:
437
-        // {
438
-        //   "token": "0c4eb0cb104a43caaeef",
439
-        //   "link": "https://cloud.seafile.com/d/db1a50e686/",
440
-        //   ...
441
-        // }
442
-
443
-        if (is_array($result) && !empty($result['link'])) {
444
-            return $result;
445
-        }
446
-
447
-        return false;
448
-    }
449
-
450
-    /**
451
-     * Delete a share (download) link
452
-     *
453
-     * @param string $token Link identifier (token)
454
-     *
455
-     * @return bool True on success, False on failure
456
-     */
457
-    public function share_link_delete($token)
458
-    {
459
-        // sanity checks
460
-        if ($token === '' || !is_string($token)) {
461
-            $this->status = self::BAD_REQUEST;
462
-            return false;
463
-        }
464
-
465
-        $this->request('DELETE', "share-links/{$token}", null, null, null, '2.1');
466
-
467
-        return $this->is_error() === false;
468
-    }
469
-
470
-    /**
471
-     * List upload links
472
-     *
473
-     * @param string $repo_id Library identifier
474
-     * @param string $dir     Directory name (with path)
475
-     *
476
-     * @return bool|array List of upload links on success, False on failure
477
-     */
478
-    public function upload_link_list($repo_id, $dir)
479
-    {
480
-        // sanity checks
481
-        if (!is_string($dir)) {
482
-            $this->status = self::BAD_REQUEST;
483
-            return false;
484
-        }
485
-
486
-        if ($repo_id === '' || !is_string($repo_id)) {
487
-            $this->status = self::BAD_REQUEST;
488
-            return false;
489
-        }
490
-
491
-        if ($dir === '') {
492
-            $dir = '/';
493
-        }
494
-
495
-        // Example result:
496
-        // [
497
-        //   {
498
-        //     "username": "lian@lian.com",
499
-        //     "repo_id": "104f6537-b3a5-4d42-b8b5-8e47e494e4cf",
500
-        //     "ctime": "2017-04-01T02:35:29+00:00",
501
-        //     "expire_date": "",
502
-        //     "token": "0c4eb0cb104a43caaeef",
503
-        //     "view_cnt": 0,
504
-        //     "link": "https://cloud.seafile.com/d/0c4eb0cb104a43caaeef/",
505
-        //     "obj_name": "folder",
506
-        //     "path": "/folder/",
507
-        //     "is_dir": true,
508
-        //     "is_expired": false,
509
-        //     "repo_name": "for-test-web-api"
510
-        //   }
511
-        // ]
512
-
513
-        return $this->request('GET', "upload-links", array('repo_id' => $repo_id, 'path' => $dir), null, null, '2.1');
514
-    }
515
-
516
-    /**
517
-     * Create an upload link
518
-     *
519
-     * @param string $repo_id  Library identifier
520
-     * @param string $dir      Directory name (with path)
521
-     * @param string $password Password
522
-     *
523
-     * @return bool Link info on success, False on failure
524
-     */
525
-    public function upload_link_add($repo_id, $dir, $password = '')
526
-    {
527
-        // sanity checks
528
-        if (!is_string($dir)) {
529
-            $this->status = self::BAD_REQUEST;
530
-            return false;
531
-        }
532
-
533
-        if ($repo_id === '' || !is_string($repo_id)) {
534
-            $this->status = self::BAD_REQUEST;
535
-            return false;
536
-        }
537
-
538
-        if ($dir === '') {
539
-            $dir = '/';
540
-        }
541
-
542
-        $post = array(
543
-            'repo_id'     => $repo_id,
544
-            'path'        => $dir,
545
-        );
546
-
547
-        if (strlen($password)) {
548
-            $post['password'] = $password;
549
-        }
550
-
551
-        $result = $this->request('POST', "upload-links", array(), $post, null, '2.1');
552
-
553
-        // Sample response:
554
-        // {
555
-        //   "token": "0c4eb0cb104a43caaeef",
556
-        //   "link": "https://cloud.seafile.com/d/db1a50e686/",
557
-        //   ...
558
-        // }
559
-
560
-        if (is_array($result) && !empty($result['link'])) {
561
-            return $result;
562
-        }
563
-
564
-        return false;
565
-    }
566
-
567
-    /**
568
-     * Delete an upload link
569
-     *
570
-     * @param string $token Link identifier (token)
571
-     *
572
-     * @return bool True on success, False on failure
573
-     */
574
-    public function upload_link_delete($token)
575
-    {
576
-        // sanity checks
577
-        if ($token === '' || !is_string($token)) {
578
-            $this->status = self::BAD_REQUEST;
579
-            return false;
580
-        }
581
-
582
-        $this->request('DELETE', "upload-links/{$token}", null, null, null, '2.1');
583
-
584
-        return $this->is_error() === false;
585
-    }
586
-
587
-    /**
588
-     * List user groups
589
-     *
590
-     * @return bool|array List of groups on success, False on failure
591
-     */
592
-    public function group_list()
593
-    {
594
-        // Sample result:
595
-        // [
596
-        //   {
597
-        //     "ctime": 1398134171327948,
598
-        //     "creator": "user@example.com",
599
-        //     "msgnum": 0,
600
-        //     "mtime": 1398231100,
601
-        //     "id": 1,
602
-        //     "name": "lian"
603
-        //   }
604
-        // ]                                                                                    },
605
-
606
-        $result = $this->request('GET', "groups");
607
-
608
-        if (is_array($result)) {
609
-            $result = (array) $result['groups'];
610
-        }
611
-
612
-        return $result;
613
-    }
614
-
615
-    /**
616
-     * List users
617
-     *
618
-     * @param string $search Search keyword
619
-     *
620
-     * @return bool|array List of users on success, False on failure
621
-     */
622
-    public function user_search($search = null)
623
-    {
624
-        // Sample response:
625
-        // [
626
-        //   {
627
-        //     'avatar_url': 'https://cloud.seafile.com/media/avatars/default.png',
628
-        //      'contact_email': 'foo@foo.com',
629
-        //      'email': 'foo@foo.com',
630
-        //      'name': 'foo'
631
-        //   }
632
-        // ]
633
-
634
-        $result = $this->request('GET', "search-user", array('q' => $search));
635
-
636
-        if (is_array($result)) {
637
-            $result = (array) $result['users'];
638
-        }
639
-
640
-        return $result;
641
-    }
642
-
643
-    /**
644
-     * Parse and fix API request URLs
645
-     */
646
-    public function mod_url($url)
647
-    {
648
-        // If Seafile is behind a proxy and different port, it will return
649
-        // wrong URL for file uploads/downloads. We force the original URL prefix here
650
-        if (stripos($url, $this->url_prefix) !== 0) {
651
-            $url = $this->url_prefix . preg_replace('|^(https?://[^/]+)|i', '', $url);
652
-        }
653
-
654
-        return $url;
655
-    }
656
 }
657
iRony-0.4.3.tar.gz/lib/FileAPI/drivers/seafile/seafile_file_storage.php -> iRony-0.4.4.tar.gz/lib/FileAPI/drivers/seafile/seafile_file_storage.php Changed
980
 
1
@@ -117,10 +117,8 @@
2
             'ssl_verify_peer' => $this->rc->config->get('fileapi_seafile_ssl_verify_peer', true),
3
             'ssl_verify_host' => $this->rc->config->get('fileapi_seafile_ssl_verify_host', true),
4
             'cache'           => $this->rc->config->get('fileapi_seafile_cache'),
5
-            'cache_ttl'       => $this->rc->config->get('fileapi_seafile_cache_ttl', '14d'),
6
+            'cache_ttl'       => $this->rc->config->get('fileapi_seafile_cache', '14d'),
7
             'debug'           => $this->rc->config->get('fileapi_seafile_debug', false),
8
-            'token'           => $_SESSION[$this->title . 'seafile_token'] ? $this->rc->decrypt($_SESSION[$this->title . 'seafile_token']) : null,
9
-
10
         );
11
 
12
         $this->config = array_merge($config, $this->config);
13
@@ -132,26 +130,11 @@
14
             return true;
15
         }
16
 
17
-        if ($this->config['cache']) {
18
-            $cache = $this->rc->get_cache('seafile_' . $this->title,
19
-                $this->config['cache'], $this->config['cache_ttl'], true);
20
-        }
21
-
22
         // try session token
23
-        if ($config['token']) {
24
-            // With caching we know the last successful token use, so we can
25
-            // skip ping call, which is a big win for case of parallel folders listing
26
-            if ($cache) {
27
-                $valid = ($ping = $cache->get('ping')) && $ping + 15 >= time();
28
-            }
29
-
30
-            if (empty($valid)) {
31
-                $valid = $this->api->ping($config['token']);
32
-
33
-                if ($cache && $valid) {
34
-                    $cache->write('ping', time());
35
-                }
36
-            }
37
+        if ($_SESSION[$this->title . 'seafile_token']
38
+            && ($token = $this->rc->decrypt($_SESSION[$this->title . 'seafile_token']))
39
+        ) {
40
+            $valid = $this->api->ping($token);
41
         }
42
 
43
         if (!$valid) {
44
@@ -168,10 +151,6 @@
45
 
46
             if ($user) {
47
                 $valid = $this->authenticate($user, $pass);
48
-
49
-                if ($cache) {
50
-                    $cache->remove('ping');
51
-                }
52
             }
53
         }
54
 
55
@@ -179,9 +158,6 @@
56
         if (!$valid && empty($_SESSION[$this->title . 'seafile_user'])) {
57
             throw new Exception("User credentials not provided", file_storage::ERROR_NOAUTH);
58
         }
59
-        else if (!$valid && !$this->api) {
60
-            throw new Exception("SeaFile storage unavailable", file_storage::ERROR);
61
-        }
62
         else if (!$valid && $this->api->is_error() == seafile_api::TOO_MANY_REQUESTS) {
63
             throw new Exception("SeaFile storage temporarily unavailable (too many requests)", file_storage::ERROR);
64
         }
65
@@ -229,7 +205,6 @@
66
             file_storage::CAPS_MAX_UPLOAD => $max_filesize,
67
             file_storage::CAPS_QUOTA      => true,
68
             file_storage::CAPS_LOCKS      => true,
69
-            file_storage::CAPS_ACL        => true,
70
         );
71
     }
72
 
73
@@ -470,7 +445,7 @@
74
      * Return file body.
75
      *
76
      * @param string   $file_name Name of a file (with folder path)
77
-     * @param array    $params    Parameters (force-download, force-type, head)
78
+     * @param array    $params    Parameters (force-download)
79
      * @param resource $fp        Print to file pointer instead (send no headers)
80
      *
81
      * @throws Exception
82
@@ -488,7 +463,7 @@
83
         $file = $this->from_file_object($file);
84
 
85
         // get file location on SeaFile server for download
86
-        if ($file['size'] && empty($params['head'])) {
87
+        if ($file['size']) {
88
             $link = $this->api->file_get($repo_id, $fn);
89
         }
90
 
91
@@ -531,20 +506,10 @@
92
         header("Content-Disposition: $disposition; filename=\"$filename\"");
93
 
94
         // just send redirect to SeaFile server
95
-        if ($file['size'] && empty($params['head'])) {
96
-            $allow_redirects = $this->rc->config->get('fileapi_seafile_allow_redirects');
97
-            // In view-mode we can't redirect to SeaFile server because:
98
-            // - it responds with Content-Disposition: attachment, which causes that
99
-            //   e.g. previewing images is not possible
100
-            // - pdf/odf viewers can't follow redirects for some reason (#4590)
101
-            if ($allow_redirects && !empty($params['force-download'])) {
102
-                header("Location: $link");
103
-            }
104
-            else if ($fp = fopen('php://output', 'wb')) {
105
-                $this->save_file_content($link, $fp);
106
-                fclose($fp);
107
-            }
108
+        if ($file['size']) {
109
+            header("Location: $link");
110
         }
111
+        die;
112
     }
113
 
114
     /**
115
@@ -570,8 +535,8 @@
116
             'name'     => $file['name'],
117
             'size'     => (int) $file['size'],
118
             'type'     => (string) $file['type'],
119
-            'mtime'    => file_utils::date_format($file['changed'], $this->config['date_format'], $this->config['timezone']),
120
-            'ctime'    => file_utils::date_format($file['created'], $this->config['date_format'], $this->config['timezone']),
121
+            'mtime'    => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
122
+            'ctime'    => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
123
             'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
124
             'created'  => $file['created'] ? $file['created']->format('U') : 0,
125
         );
126
@@ -588,11 +553,6 @@
127
      */
128
     public function file_list($folder_name, $params = array())
129
     {
130
-        // mount point contains only folders
131
-        if (!is_string($folder_name) || $folder_name === '') {
132
-            return array();
133
-        }
134
-
135
         list($folder, $repo_id) = $this->find_library($folder_name);
136
 
137
         // prepare search filter
138
@@ -608,7 +568,7 @@
139
         }
140
 
141
         // get directory entries
142
-        $entries = $this->api->directory_entries($repo_id, $folder, 'file');
143
+        $entries = $this->api->directory_entries($repo_id, $folder);
144
         $result  = array();
145
 
146
         foreach ((array) $entries as $idx => $file) {
147
@@ -628,8 +588,8 @@
148
                     }
149
                     else if ($idx == 'class') {
150
                         foreach ($value as $v) {
151
-                            if (stripos($file['type'], $v) !== false) {
152
-                                continue 2;
153
+                            if (stripos($file['type'], $v) === 0) {
154
+                                break 2;
155
                             }
156
                         }
157
 
158
@@ -644,8 +604,8 @@
159
                 'name'     => $file['name'],
160
                 'size'     => (int) $file['size'],
161
                 'type'     => (string) $file['type'],
162
-                'mtime'    => file_utils::date_format($file['changed'], $this->config['date_format'], $this->config['timezone']),
163
-                'ctime'    => file_utils::date_format($file['created'], $this->config['date_format'], $this->config['timezone']),
164
+                'mtime'    => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
165
+                'ctime'    => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
166
                 'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
167
                 'created'  => $file['created'] ? $file['created']->format('U') : 0,
168
             );
169
@@ -854,49 +814,23 @@
170
     }
171
 
172
     /**
173
-     * Subscribe a folder.
174
-     *
175
-     * @param string $folder_name Name of a folder with full path
176
-     *
177
-     * @throws Exception
178
-     */
179
-    public function folder_subscribe($folder_name)
180
-    {
181
-        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
182
-    }
183
-
184
-    /**
185
-     * Unsubscribe a folder.
186
-     *
187
-     * @param string $folder_name Name of a folder with full path
188
-     *
189
-     * @throws Exception
190
-     */
191
-    public function folder_unsubscribe($folder_name)
192
-    {
193
-        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
194
-    }
195
-
196
-    /**
197
      * Returns list of folders.
198
      *
199
-     * @param array $params List parameters ('type', 'search', 'path', 'level')
200
-     *
201
      * @return array List of folders
202
      * @throws Exception
203
      */
204
-    public function folder_list($params = array())
205
+    public function folder_list()
206
     {
207
-        $libraries  = $this->libraries();
208
-        $writable   = ($params['type'] & file_storage::FILTER_WRITABLE) ? true : false;
209
-        $prefix     = (string) $params['path'];
210
-        $prefix_len = strlen($prefix);
211
-        $folders    = array();
212
+        $libraries = $this->libraries();
213
+        $folders   = array();
214
+
215
+        if ($this->config['cache']) {
216
+            $cache = $this->rc->get_cache('seafile_' . $this->title,
217
+                $this->config['cache'], $this->config['cache_ttl'], true);
218
 
219
-        if ($prefix_len) {
220
-            $path           = explode('/', $prefix);
221
-            $lib_search     = array_shift($path);
222
-            $params['path'] = implode('/', $path);
223
+            if ($cache) {
224
+                $cached = $cache->get('folders');
225
+            }
226
         }
227
 
228
         foreach ($libraries as $library) {
229
@@ -904,110 +838,29 @@
230
                 continue;
231
             }
232
 
233
-            if ($prefix_len && $lib_search !== $library['name']) {
234
-                continue;
235
-            }
236
-
237
-            if (!strlen($params['path'])) {
238
-                $folders[$library['name']] = array(
239
-                    'mtime'      => $library['mtime'],
240
-                    'permission' => $library['permission'],
241
-                );
242
-            }
243
+            $folders[$library['name']] = $library['mtime'];
244
 
245
-            if ($params['level'] == 1 && !$prefix_len) {
246
-                // Only list of libraries has been requested
247
-                continue;
248
-            }
249
-
250
-            foreach ($this->folders_tree($library, $params) as $folder_name => $folder) {
251
-                $folders[$library['name'] . '/' . $folder_name] = $folder;
252
+            if ($folder_tree = $this->folders_tree($library, '', $library, $cached)) {
253
+                $folders = array_merge($folders, $folder_tree);
254
             }
255
         }
256
 
257
-        if (empty($libraries)) {
258
+        if (empty($folders)) {
259
             throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR);
260
         }
261
 
262
-        // remove read-only folders when requested
263
-        if ($writable) {
264
-            foreach ($folders as $folder_name => $folder) {
265
-                if (strpos($folder['permission'], 'w') === false) {
266
-                    unset($folders[$folder_name]);
267
-                }
268
-            }
269
-        }
270
-
271
-        // In extended format we return array of arrays
272
-        if (!empty($params['extended'])) {
273
-            foreach ($folders as $folder_name => $folder) {
274
-                $item = array('folder' => $folder_name);
275
-
276
-                // check if folder is readonly
277
-                if (!$writable && $params['permissions']) {
278
-                    if (strpos($folder['permission'], 'w') === false) {
279
-                        $item['readonly'] = true;
280
-                    }
281
-                }
282
-
283
-                $folders[$folder_name] = $item;
284
-            }
285
-        }
286
-        else {
287
-            $folders = array_keys($folders);
288
+        if ($cache) {
289
+            $cache->set('folders', $folders);
290
         }
291
 
292
         // sort folders
293
-        usort($folders, array('file_utils', 'sort_folder_comparator'));
294
+        $folders = array_keys($folders);
295
+        usort($folders, array($this, 'sort_folder_comparator'));
296
 
297
         return $folders;
298
     }
299
 
300
     /**
301
-     * Check folder rights.
302
-     *
303
-     * @param string $folder_name Name of a folder with full path
304
-     *
305
-     * @return int Folder rights (sum of file_storage::ACL_*)
306
-     */
307
-    public function folder_rights($folder_name)
308
-    {
309
-        // It is not possible (yet) to assign a specified library/folder
310
-        // to the mount point. So, it is a "virtual" folder.
311
-        if (!strlen($folder_name)) {
312
-            return 0;
313
-        }
314
-
315
-        list($folder, $repo_id, $library) = $this->find_library($folder_name);
316
-
317
-        // @TODO: we should check directory permission not library
318
-        // However, there's no API for this, we'd need to get a list
319
-        // of directories of a parent folder/library
320
-/*
321
-        if (strpos($folder, '/')) {
322
-            // @TODO
323
-        }
324
-        else {
325
-            $acl = $library['permission'];
326
-        }
327
-*/
328
-        $acl    = $library['permission'];
329
-        $rights = 0;
330
-        $map    = array(
331
-            'r' => file_storage::ACL_READ,
332
-            'w' => file_storage::ACL_WRITE,
333
-        );
334
-
335
-        foreach ($map as $key => $value) {
336
-            if (strpos($acl, $key) !== false) {
337
-                $rights |= $value;
338
-            }
339
-        }
340
-
341
-        return $rights;
342
-    }
343
-
344
-    /**
345
      * Returns a list of locks
346
      *
347
      * This method should return all the locks for a particular URI, including
348
@@ -1016,25 +869,25 @@
349
      * If child_locks is set to true, this method should also look for
350
      * any locks in the subtree of the URI for locks.
351
      *
352
-     * @param string $path        File/folder path
353
+     * @param string $uri         URI
354
      * @param bool   $child_locks Enables subtree checks
355
      *
356
      * @return array List of locks
357
      * @throws Exception
358
      */
359
-    public function lock_list($path, $child_locks = false)
360
+    public function lock_list($uri, $child_locks = false)
361
     {
362
         $this->init_lock_db();
363
 
364
         // convert URI to global resource string
365
-        $uri = $this->path2uri($path);
366
+        $uri = $this->uri2resource($uri);
367
 
368
         // get locks list
369
         $list = $this->lock_db->lock_list($uri, $child_locks);
370
 
371
         // convert back resource string into URIs
372
         foreach ($list as $idx => $lock) {
373
-            $list[$idx]['uri'] = $this->uri2path($lock['uri']);
374
+            $list[$idx]['uri'] = $this->resource2uri($lock['uri']);
375
         }
376
 
377
         return $list;
378
@@ -1043,7 +896,7 @@
379
     /**
380
      * Locks a URI
381
      *
382
-     * @param string $path File/folder path
383
+     * @param string $uri  URI
384
      * @param array  $lock Lock data
385
      *                     - depth: 0/'infinite'
386
      *                     - scope: 'shared'/'exclusive'
387
@@ -1053,12 +906,12 @@
388
      *
389
      * @throws Exception
390
      */
391
-    public function lock($path, $lock)
392
+    public function lock($uri, $lock)
393
     {
394
         $this->init_lock_db();
395
 
396
         // convert URI to global resource string
397
-        $uri = $this->uri2resource($path);
398
+        $uri = $this->uri2resource($uri);
399
 
400
         if (!$this->lock_db->lock($uri, $lock)) {
401
             throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR);
402
@@ -1068,17 +921,17 @@
403
     /**
404
      * Removes a lock from a URI
405
      *
406
-     * @param string $path File/folder path
407
+     * @param string $path URI
408
      * @param array  $lock Lock data
409
      *
410
      * @throws Exception
411
      */
412
-    public function unlock($path, $lock)
413
+    public function unlock($uri, $lock)
414
     {
415
         $this->init_lock_db();
416
 
417
         // convert URI to global resource string
418
-        $uri = $this->path2uri($path);
419
+        $uri = $this->uri2resource($uri);
420
 
421
         if (!$this->lock_db->unlock($uri, $lock)) {
422
             throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR);
423
@@ -1115,416 +968,63 @@
424
     }
425
 
426
     /**
427
-     * Sharing interface
428
-     *
429
-     * @param string $folder_name Name of a folder with full path
430
-     * @param int    $mode        Sharing action mode
431
-     * @param array  $args        POST/GET parameters
432
-     *
433
-     * @return mixed Sharing response
434
-     * @throws Exception
435
+     * Recursively builds folders list
436
      */
437
-    public function sharing($folder, $mode, $args = array())
438
+    protected function folders_tree($library, $path, $folder, $cached)
439
     {
440
-        if ($mode == file_storage::SHARING_MODE_FORM) {
441
-            $form = array(
442
-                'shares' => array(
443
-                    'title' => 'share.permissions',
444
-                    'form'  => array(
445
-                        'user' => array(
446
-                            'title' => 'share.usergroup',
447
-                            'type'  => 'input',
448
-                            'autocomplete' => 'user,group',
449
-                        ),
450
-                        'right' => array(
451
-                            'title' => 'share.permission',
452
-                            'type'  => 'select',
453
-                            'options' => array(
454
-                                'r'  => 'share.readonly',
455
-                                'rw' => 'share.readwrite',
456
-                            ),
457
-                        ),
458
-                    ),
459
-                    'extra_fields' => array(
460
-                        'type' => 'user',
461
-                        'id'   => '',
462
-                    ),
463
-                ),
464
-                'download-link' => array(
465
-                    'title'             => 'share.download-link',
466
-                    'label'             => 'share.generate',
467
-                    'single'            => true,
468
-                    'list_column'       => 'link',
469
-                    'list_column_label' => 'share.link',
470
-                    'form'              => array(
471
-                        'password' => array(
472
-                            'title'       => 'share.password',
473
-                            'type'        => 'password',
474
-                        ),
475
-                        'expire' => array(
476
-                            'title'       => 'share.expire',
477
-                            'placeholder' => 'share.expiredays',
478
-                            'type'        => 'input',
479
-                        ),
480
-                    ),
481
-                    'extra_fields' => array(
482
-                        'id' => '',
483
-                    ),
484
-                ),
485
-                'upload-link' => array(
486
-                    'title'             => 'share.upload-link',
487
-                    'label'             => 'share.generate',
488
-                    'single'            => true,
489
-                    'list_column'       => 'link',
490
-                    'list_column_label' => 'share.link',
491
-                    'form'              => array(
492
-                        'password' => array(
493
-                            'title' => 'share.password',
494
-                            'type'  => 'password',
495
-                        ),
496
-                    ),
497
-                    'extra_fields' => array(
498
-                        'id' => '',
499
-                    ),
500
-                ),
501
-            );
502
-
503
-            return $form;
504
-        }
505
-
506
-        if ($mode == file_storage::SHARING_MODE_RIGHTS) {
507
-            if (!$this->init()) {
508
-                throw new Exception("Storage error. Unable to get shares of SeaFile folder/lib.", file_storage::ERROR);
509
-            }
510
-
511
-            list($path, $repo_id) = $this->find_library($folder);
512
-
513
-            $result = array();
514
-
515
-            if ($shares = $this->api->shared_item_list($repo_id, $path)) {
516
-                foreach ($shares as $share) {
517
-                    if (!empty($share['group_info'])) {
518
-                        $name = $share['group_info']['name'];
519
-                        $name = $share['group_info']['id'];
520
-                    }
521
-                    else {
522
-                        $name = $this->user_label($share['user_info']['nickname'], $share['user_info']['name']);
523
-                        $id   = $share['user_info']['name'];
524
-                    }
525
-
526
-                    $result[] = array(
527
-                        'mode'  => 'shares',
528
-                        'type'  => $share['share_type'],
529
-                        'right' => $share['permission'],
530
-                        'user'  => $name,
531
-                        'id'    => $id,
532
-                    );
533
-                }
534
-            }
535
-
536
-            if ($links = $this->api->share_link_list($repo_id, $path)) {
537
-                foreach ($links as $link) {
538
-                    $result[] = array(
539
-                        'mode' => 'download-link',
540
-                        'id'   => $link['token'],
541
-                        'link' => $link['link'],
542
-                    );
543
-                }
544
-            }
545
-
546
-            if ($links = $this->api->upload_link_list($repo_id, $path)) {
547
-                foreach ($links as $link) {
548
-                    $result[] = array(
549
-                        'mode' => 'upload-link',
550
-                        'id'   => $link['token'],
551
-                        'link' => $link['link'],
552
-                    );
553
+        $folders = array();
554
+        $fname  = strlen($path) ? $path . $folder['name'] : '/';
555
+        $root   = $library['name'] . ($fname != '/' ? $fname : '');
556
+
557
+        // nothing changed, use cached folders tree of this folder
558
+        if ($cached && $cached[$root] && $cached[$root] == $folder['mtime']) {
559
+            foreach ($cached as $folder_name => $mtime) {
560
+                if (strpos($folder_name, $root . '/') === 0) {
561
+                    $folders[$folder_name] = $mtime;
562
                 }
563
             }
564
-
565
-            return $result;
566
         }
567
-
568
-        if ($mode == file_storage::SHARING_MODE_UPDATE) {
569
-            if (!$this->init()) {
570
-                throw new Exception("Storage error. Unable to update shares of SeaFile folder/lib.", file_storage::ERROR);
571
+        // get folder content (files and sub-folders)
572
+        // there's no API method to get only folders
573
+        else if ($content = $this->api->directory_entries($library['id'], $fname)) {
574
+            if ($fname != '/') {
575
+                $fname .= '/';
576
             }
577
 
578
-            list($path, $repo_id) = $this->find_library($folder);
579
-
580
-            if ($args['mode'] == 'shares') {
581
-                $share_id = $args['id'] ?: $args['user'];
582
-
583
-                switch ($args['action']) {
584
-                case 'submit':
585
-                    $result = $this->api->shared_item_add($repo_id, $path, $args['right'], $args['type'], $share_id);
586
-                    break;
587
-
588
-                case 'update':
589
-                    $result = $this->api->shared_item_update($repo_id, $path, $args['right'], $args['type'], $share_id);
590
-                    break;
591
-
592
-                case 'delete':
593
-                    $result = $this->api->shared_item_delete($repo_id, $path, $args['type'], $share_id);
594
-                    break;
595
-                }
596
-            }
597
-            else if ($args['mode'] == 'download-link') {
598
-                switch ($args['action']) {
599
-                case 'submit':
600
-                    $result = $this->api->share_link_add($repo_id, $path, $args['password'], $args['expire']);
601
-
602
-                    if ($result) {
603
-                        $result['id']   = $result['token'];
604
-                        $result['mode'] = 'download-link';
605
-                    }
606
-                    break;
607
-
608
-                case 'delete':
609
-                    $result = $this->api->share_link_delete($args['id']);
610
-                    break;
611
-                }
612
-            }
613
-            else if ($args['mode'] == 'upload-link') {
614
-                switch ($args['action']) {
615
-                case 'submit':
616
-                    $result = $this->api->upload_link_add($repo_id, $path, $args['password']);
617
-
618
-                    if ($result) {
619
-                        $result['id']   = $result['token'];
620
-                        $result['mode'] = 'upload-link';
621
-                    }
622
-                    break;
623
-
624
-                case 'delete':
625
-                    $result = $this->api->upload_link_delete($args['id']);
626
-                    break;
627
-                }
628
-            }
629
-            else {
630
-                throw new Exception("Invalid input.", file_storage::ERROR);
631
-            }
632
-
633
-            if (empty($result)) {
634
-                throw new Exception("Storage error. Failed to update share.", file_storage::ERROR);
635
-            }
636
-
637
-            return $result;
638
-        }
639
-    }
640
-
641
-    /**
642
-     * User/group search (autocompletion)
643
-     *
644
-     * @param string $search Search string
645
-     * @param int    $mode   Search mode
646
-     *
647
-     * @return array Users/Groups list
648
-     * @throws Exception
649
-     */
650
-    public function autocomplete($search, $mode)
651
-    {
652
-        if (!$this->init()) {
653
-            throw new Exception("Storage error. Failed to init Seafile storage connection.", file_storage::ERROR);
654
-        }
655
-
656
-        $limit  = (int) $this->rc->config->get('autocomplete_max', 15);
657
-        $result = array();
658
-        $index  = array();
659
-
660
-        if ($mode & file_storage::SEARCH_USER) {
661
-            $users = $this->api->user_search($search);
662
-
663
-            if (!is_array($users)) {
664
-                throw new Exception("Storage error. Failed to search users.", file_storage::ERROR);
665
-            }
666
-
667
-            foreach ($users as $user) {
668
-                $index[] = $user['name'];
669
-                $result[] = array(
670
-                    'name' => $this->user_label($user['name'], $user['email']),
671
-                    'id'   => $user['email'],
672
-                    'type' => 'user',
673
-                );
674
-            }
675
-        }
676
+            foreach ($content as $item) {
677
+                if ($item['type'] == 'dir' && strlen($item['name'])) {
678
+                    $folders[$root . '/' . $item['name']] = $item['mtime'];
679
 
680
-        if (count($result) < $limit && ($mode & file_storage::SEARCH_GROUP)) {
681
-            if ($groups = $this->api->group_list()) {
682
-                $search = mb_strtoupper($search);
683
-                foreach ($groups as $group) {
684
-                    if (strpos(mb_strtoupper($group['name']), $search) !== false) {
685
-                        $index[] = $group['name'];
686
-                        $result[] = array(
687
-                            'name' => $group['name'],
688
-                            'id'   => $group['id'],
689
-                            'type' => 'group',
690
-                        );
691
+                    // get subfolders recursively
692
+                    $folders_tree = $this->folders_tree($library, $fname, $item, $cached);
693
+                    if (!empty($folders_tree)) {
694
+                        $folders = array_merge($folders, $folders_tree);
695
                     }
696
                 }
697
             }
698
         }
699
 
700
-        if (count($result)) {
701
-            array_multisort($index, SORT_ASC, SORT_LOCALE_STRING, $result);
702
-        }
703
-
704
-        if (count($result) > $limit) {
705
-            $result = array_slice($result, 0, $limit);
706
-        }
707
-
708
-        return $result;
709
-    }
710
-
711
-    /**
712
-     * Convert file/folder path into a global URI.
713
-     *
714
-     * @param string $path File/folder path
715
-     *
716
-     * @return string URI
717
-     * @throws Exception
718
-     */
719
-    public function path2uri($path)
720
-    {
721
-        // Remove protocol prefix and path, we work with host only
722
-        $host = preg_replace('#(^https?://|/.*$)#i', '', $this->config['host']);
723
-
724
-        if (!is_string($path) || !strlen($path)) {
725
-            $user = $_SESSION[$this->title . 'seafile_user'];
726
-            return 'seafile://' . rawurlencode($user) . '@' . $host . '/';
727
-        }
728
-
729
-        list($file, $repo_id, $library) = $this->find_library($path);
730
-
731
-        return 'seafile://' . rawurlencode($library['owner']) . '@' . $host . '/' . file_utils::encode_path($path);
732
-    }
733
-
734
-    /**
735
-     * Convert global URI into file/folder path.
736
-     *
737
-     * @param string $uri URI
738
-     *
739
-     * @return string File/folder path
740
-     * @throws Exception
741
-     */
742
-    public function uri2path($uri)
743
-    {
744
-        if (!preg_match('|^seafile://([^@]+)@([^/]+)/(.*)$|', $uri, $matches)) {
745
-            throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR);
746
-        }
747
-
748
-        $user   = rawurldecode($matches[1]);
749
-        $host   = $matches[2];
750
-        $path   = file_utils::decode_path($matches[3]);
751
-        $c_host = preg_replace('#(^https?://|/.*$)#i', '', $this->config['host']);
752
-
753
-        if (strlen($path)) {
754
-            list($file, $repo_id, $library) = $this->find_library($path, true);
755
-
756
-            if (empty($library) || $host != $c_host || $user != $library['owner']) {
757
-                throw new Exception("Internal storage error. Unresolvable URI.", file_storage::ERROR);
758
-            }
759
-        }
760
-
761
-        return $path;
762
+        return $folders;
763
     }
764
 
765
     /**
766
-     * Get folders tree in the Seafile library
767
+     * Callback for uasort() that implements correct
768
+     * locale-aware case-sensitive sorting
769
      */
770
-    protected function folders_tree($library, $params = array())
771
+    protected function sort_folder_comparator($str1, $str2)
772
     {
773
-        $root = '';
774
-        if (is_string($params['path']) && strlen($params['path'])) {
775
-            $root = trim($params['path'], '/');
776
-        }
777
+        $path1 = explode('/', $str1);
778
+        $path2 = explode('/', $str2);
779
 
780
-        if ($this->config['cache'] && empty($params['recursive'])) {
781
-            $cache = $this->rc->get_cache('seafile_' . $this->title,
782
-                $this->config['cache'], $this->config['cache_ttl'], true);
783
+        foreach ($path1 as $idx => $folder1) {
784
+            $folder2 = $path2[$idx];
785
 
786
-            if ($cache) {
787
-                $cache_key = 'folders.' . md5(sprintf('%s:%d:%s', $library['id'], $params['level'], $root));
788
-                $folders   = $cache->get($cache_key);
789
-
790
-                if (is_string($folders) && preg_match('/^([0-9]+):[\{\[]/', $folders, $m)) {
791
-                    $cache_mtime = $m[1];
792
-                    $folders     = json_decode(substr($folders, strlen($cache_mtime)+1), true);
793
-                }
794
-                else {
795
-                    $folders = null;
796
-                }
797
-
798
-                if (strlen($root)) {
799
-                    $info = $this->api->directory_info($library['id'], $root);
800
-                    if ($info && $info['mtime']) {
801
-                        try {
802
-                            $dt    = new DateTime($info['mtime']);
803
-                            $mtime = $dt->format('U');
804
-                        }
805
-                        catch (Exception $e) {
806
-                            // ignore
807
-                            rcube::raise_error($e, true, false);
808
-                        }
809
-                    }
810
-                }
811
-                else {
812
-                    $mtime = $library['mtime'];
813
-                }
814
-
815
-                if (is_array($folders) && $mtime && $cache_mtime && intval($mtime) === intval($cache_mtime)) {
816
-                    return $folders;
817
-                }
818
-            }
819
-        }
820
-
821
-        $folders    = array();
822
-        $add_folder = function($item, &$result, $parent) {
823
-            if ($item['type'] == 'dir' && strlen($item['name'])) {
824
-                $name = (strlen($parent) > 0 ? "$parent/" : '') . $item['name'];
825
-
826
-                $result[$name] = array(
827
-                    'mtime'      => $item['mtime'],
828
-                    'permission' => $item['permission'],
829
-                );
830
-
831
-                return $name;
832
-            }
833
-        };
834
-
835
-        // Full folder hierarchy requested, we can get all in one request...
836
-        if (empty($params['recursive']) && empty($params['level'])) {
837
-            if ($content = $this->api->directory_entries($library['id'], $root, 'dir', true)) {
838
-                foreach ($content as $item) {
839
-                    $add_folder($item, $folders, trim($item['parent_dir'], '/'));
840
-                }
841
-            }
842
-        }
843
-        // Only part of the folder tree has been requested...
844
-        else if ($content = $this->api->directory_entries($library['id'], $root, 'dir', false)) {
845
-            $params['recursive'] = true;
846
-            $params['level']    -= 1;
847
-
848
-            // Recursively add sub-folders tree
849
-            foreach ($content as $item) {
850
-                $folder = $add_folder($item, $folders, $root);
851
-
852
-                // FIXME: id="0000000000000000000000000000000000000000" means the folder is empty?
853
-                if ($folder !== null && $params['level'] > 1 && $item['id'] !== "0000000000000000000000000000000000000000") {
854
-                    $params['path'] = $folder;
855
-                    $tree = $this->folders_tree($library, $params);
856
-                    if (!empty($tree)) {
857
-                        $folders = array_merge($folders, $tree);
858
-                    }
859
-                }
860
+            if ($folder1 === $folder2) {
861
+                continue;
862
             }
863
-        }
864
 
865
-        if ($cache_key && is_array($content) && $mtime && ($_folders = json_encode($folders))) {
866
-            $cache->set($cache_key, intval($mtime) . ':' . $_folders);
867
+            return strcoll($folder1, $folder2);
868
         }
869
-
870
-        return $folders;
871
     }
872
 
873
     /**
874
@@ -1532,7 +1032,7 @@
875
      */
876
     protected function libraries()
877
     {
878
-        // get from memory
879
+        // get from memory, @TODO: cache in rcube_cache?
880
         if ($this->libraries !== null) {
881
             return $this->libraries;
882
         }
883
@@ -1541,33 +1041,8 @@
884
             throw new Exception("Storage error. Unable to get list of SeaFile libraries.", file_storage::ERROR);
885
         }
886
 
887
-        if ($this->config['cache']) {
888
-            $cache = $this->rc->get_cache('seafile_' . $this->title,
889
-                $this->config['cache'], $this->config['cache_ttl'], true);
890
-
891
-            if ($cache) {
892
-                $repos = $cache->get('repos');
893
-
894
-                if (is_string($repos) && preg_match('/^([0-9]+):[\{\[]/', $repos, $m)) {
895
-                    $mtime = $m[1];
896
-                    $repos = json_decode(substr($repos, strlen($mtime)+1), true);
897
-                    // We use the cached value for up to 15 seconds
898
-                    // It should be enough to improve parallel folders listing requests
899
-                    if (is_array($repos) && $mtime + 15 >= time()) {
900
-                        return $repos;
901
-                    }
902
-                }
903
-            }
904
-        }
905
-
906
-        $mtime = time();
907
-
908
         if ($list = $this->api->library_list()) {
909
             $this->libraries = $list;
910
-
911
-            if ($cache) {
912
-                $cache->write('repos', $mtime . ':' . json_encode($list));
913
-            }
914
         }
915
         else {
916
             $this->libraries = array();
917
@@ -1685,7 +1160,7 @@
918
         $observer->set_fp($fp);
919
 
920
         try {
921
-            $request->setUrl($this->api->mod_url($location));
922
+            $request->setUrl($location);
923
             $request->attach($observer);
924
 
925
             $response = $request->send();
926
@@ -1706,28 +1181,38 @@
927
         return true;
928
     }
929
 
930
-    /**
931
-     * Initializes file_locks object
932
-     */
933
-    protected function init_lock_db()
934
+    protected function uri2resource($uri)
935
     {
936
-        if (!$this->lock_db) {
937
-            $this->lock_db = new file_locks;
938
+        list($file, $repo_id, $library) = $this->find_library($uri);
939
+
940
+        // convert to imap charset (to be safe to store in DB)
941
+        $uri = rcube_charset::convert($uri, RCUBE_CHARSET, 'UTF7-IMAP');
942
+
943
+        return 'seafile://' . urlencode($library['owner']) . '@' . $this->config['host'] . '/' . $uri;
944
+    }
945
+
946
+    protected function resource2uri($resource)
947
+    {
948
+        if (!preg_match('|^seafile://([^@]+)@([^/]+)/(.*)$|', $resource, $matches)) {
949
+            throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR);
950
         }
951
+
952
+        $user = urldecode($matches[1]);
953
+        $uri  = $matches[3];
954
+
955
+        // convert from imap charset (to be safe to store in DB)
956
+        $uri = rcube_charset::convert($uri, 'UTF7-IMAP', RCUBE_CHARSET);
957
+
958
+        return $uri;
959
     }
960
 
961
     /**
962
-     * Create display-username-with-email string
963
+     * Initializes file_locks object
964
      */
965
-    protected function user_label($name, $email)
966
+    protected function init_lock_db()
967
     {
968
-        if ($name && $name != $email) {
969
-            $label = "{$name} ({$email})";
970
-        }
971
-        else {
972
-            $label = $email;
973
+        if (!$this->lock_db) {
974
+            $this->lock_db = new file_locks;
975
         }
976
-
977
-        return $label;
978
     }
979
 }
980
iRony-0.4.3.tar.gz/lib/FileAPI/drivers/seafile/seafile_request_observer.php -> iRony-0.4.4.tar.gz/lib/FileAPI/drivers/seafile/seafile_request_observer.php Changed
13
 
1
@@ -25,8 +25,9 @@
2
         switch ($event['name']) {
3
         case 'receivedHeaders':
4
             if ($this->file) {
5
-                if (!($this->fp = @fopen($this->file, 'wb'))) {
6
-                    throw new Exception("Cannot open target file '{$this->file}'");
7
+                $target = $this->dir . DIRECTORY_SEPARATOR . $this->file;
8
+                if (!($this->fp = @fopen($target, 'wb'))) {
9
+                    throw new Exception("Cannot open target file '{$target}'");
10
                 }
11
             }
12
             else if (!$this->fp) {
13
iRony-0.4.4.tar.gz/lib/FileAPI/ext Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Auth Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Auth/SASL Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Auth/SASL.php Added
106
 
1
@@ -0,0 +1,104 @@
2
+<?php
3
+// +-----------------------------------------------------------------------+ 
4
+// | Copyright (c) 2002-2003 Richard Heyes                                 | 
5
+// | All rights reserved.                                                  | 
6
+// |                                                                       | 
7
+// | Redistribution and use in source and binary forms, with or without    | 
8
+// | modification, are permitted provided that the following conditions    | 
9
+// | are met:                                                              | 
10
+// |                                                                       | 
11
+// | o Redistributions of source code must retain the above copyright      | 
12
+// |   notice, this list of conditions and the following disclaimer.       | 
13
+// | o Redistributions in binary form must reproduce the above copyright   | 
14
+// |   notice, this list of conditions and the following disclaimer in the | 
15
+// |   documentation and/or other materials provided with the distribution.| 
16
+// | o The names of the authors may not be used to endorse or promote      | 
17
+// |   products derived from this software without specific prior written  | 
18
+// |   permission.                                                         | 
19
+// |                                                                       | 
20
+// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   | 
21
+// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     | 
22
+// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 
23
+// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  | 
24
+// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 
25
+// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      | 
26
+// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 
27
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 
28
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   | 
29
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
30
+// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  | 
31
+// |                                                                       | 
32
+// +-----------------------------------------------------------------------+ 
33
+// | Author: Richard Heyes <richard@php.net>                               | 
34
+// +-----------------------------------------------------------------------+ 
35
+// 
36
+// $Id$
37
+
38
+/**
39
+* Client implementation of various SASL mechanisms 
40
+*
41
+* @author  Richard Heyes <richard@php.net>
42
+* @access  public
43
+* @version 1.0
44
+* @package Auth_SASL
45
+*/
46
+
47
+require_once('PEAR.php');
48
+
49
+class Auth_SASL
50
+{
51
+    /**
52
+    * Factory class. Returns an object of the request
53
+    * type.
54
+    *
55
+    * @param string $type One of: Anonymous
56
+    *                             Plain
57
+    *                             CramMD5
58
+    *                             DigestMD5
59
+    *                     Types are not case sensitive
60
+    */
61
+    function &factory($type)
62
+    {
63
+        switch (strtolower($type)) {
64
+            case 'anonymous':
65
+                $filename  = 'Auth/SASL/Anonymous.php';
66
+                $classname = 'Auth_SASL_Anonymous';
67
+                break;
68
+
69
+            case 'login':
70
+                $filename  = 'Auth/SASL/Login.php';
71
+                $classname = 'Auth_SASL_Login';
72
+                break;
73
+
74
+            case 'plain':
75
+                $filename  = 'Auth/SASL/Plain.php';
76
+                $classname = 'Auth_SASL_Plain';
77
+                break;
78
+
79
+            case 'external':
80
+                $filename  = 'Auth/SASL/External.php';
81
+                $classname = 'Auth_SASL_External';
82
+                break;
83
+
84
+            case 'crammd5':
85
+                $filename  = 'Auth/SASL/CramMD5.php';
86
+                $classname = 'Auth_SASL_CramMD5';
87
+                break;
88
+
89
+            case 'digestmd5':
90
+                $filename  = 'Auth/SASL/DigestMD5.php';
91
+                $classname = 'Auth_SASL_DigestMD5';
92
+                break;
93
+
94
+            default:
95
+                return PEAR::raiseError('Invalid SASL mechanism type');
96
+                break;
97
+        }
98
+
99
+        require_once($filename);
100
+        $obj = new $classname();
101
+        return $obj;
102
+    }
103
+}
104
+
105
+?>
106
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Auth/SASL/Anonymous.php Added
74
 
1
@@ -0,0 +1,71 @@
2
+<?php
3
+// +-----------------------------------------------------------------------+ 
4
+// | Copyright (c) 2002-2003 Richard Heyes                                 | 
5
+// | All rights reserved.                                                  | 
6
+// |                                                                       | 
7
+// | Redistribution and use in source and binary forms, with or without    | 
8
+// | modification, are permitted provided that the following conditions    | 
9
+// | are met:                                                              | 
10
+// |                                                                       | 
11
+// | o Redistributions of source code must retain the above copyright      | 
12
+// |   notice, this list of conditions and the following disclaimer.       | 
13
+// | o Redistributions in binary form must reproduce the above copyright   | 
14
+// |   notice, this list of conditions and the following disclaimer in the | 
15
+// |   documentation and/or other materials provided with the distribution.| 
16
+// | o The names of the authors may not be used to endorse or promote      | 
17
+// |   products derived from this software without specific prior written  | 
18
+// |   permission.                                                         | 
19
+// |                                                                       | 
20
+// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   | 
21
+// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     | 
22
+// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 
23
+// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  | 
24
+// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 
25
+// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      | 
26
+// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 
27
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 
28
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   | 
29
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
30
+// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  | 
31
+// |                                                                       | 
32
+// +-----------------------------------------------------------------------+ 
33
+// | Author: Richard Heyes <richard@php.net>                               | 
34
+// +-----------------------------------------------------------------------+ 
35
+// 
36
+// $Id$
37
+
38
+/**
39
+* Implmentation of ANONYMOUS SASL mechanism
40
+*
41
+* @author  Richard Heyes <richard@php.net>
42
+* @access  public
43
+* @version 1.0
44
+* @package Auth_SASL
45
+*/
46
+
47
+require_once('Auth/SASL/Common.php');
48
+
49
+class Auth_SASL_Anonymous extends Auth_SASL_Common
50
+{
51
+    /**
52
+    * Not much to do here except return the token supplied.
53
+    * No encoding, hashing or encryption takes place for this
54
+    * mechanism, simply one of:
55
+    *  o An email address
56
+    *  o An opaque string not containing "@" that can be interpreted
57
+    *    by the sysadmin
58
+    *  o Nothing
59
+    *
60
+    * We could have some logic here for the second option, but this
61
+    * would by no means create something interpretable.
62
+    *
63
+    * @param  string $token Optional email address or string to provide
64
+    *                       as trace information.
65
+    * @return string        The unaltered input token
66
+    */
67
+    function getResponse($token = '')
68
+    {
69
+        return $token;
70
+    }
71
+}
72
+?>
73
\ No newline at end of file
74
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Auth/SASL/Common.php Added
76
 
1
@@ -0,0 +1,74 @@
2
+<?php
3
+// +-----------------------------------------------------------------------+ 
4
+// | Copyright (c) 2002-2003 Richard Heyes                                 | 
5
+// | All rights reserved.                                                  | 
6
+// |                                                                       | 
7
+// | Redistribution and use in source and binary forms, with or without    | 
8
+// | modification, are permitted provided that the following conditions    | 
9
+// | are met:                                                              | 
10
+// |                                                                       | 
11
+// | o Redistributions of source code must retain the above copyright      | 
12
+// |   notice, this list of conditions and the following disclaimer.       | 
13
+// | o Redistributions in binary form must reproduce the above copyright   | 
14
+// |   notice, this list of conditions and the following disclaimer in the | 
15
+// |   documentation and/or other materials provided with the distribution.| 
16
+// | o The names of the authors may not be used to endorse or promote      | 
17
+// |   products derived from this software without specific prior written  | 
18
+// |   permission.                                                         | 
19
+// |                                                                       | 
20
+// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   | 
21
+// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     | 
22
+// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 
23
+// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  | 
24
+// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 
25
+// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      | 
26
+// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 
27
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 
28
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   | 
29
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
30
+// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  | 
31
+// |                                                                       | 
32
+// +-----------------------------------------------------------------------+ 
33
+// | Author: Richard Heyes <richard@php.net>                               | 
34
+// +-----------------------------------------------------------------------+ 
35
+// 
36
+// $Id$
37
+
38
+/**
39
+* Common functionality to SASL mechanisms
40
+*
41
+* @author  Richard Heyes <richard@php.net>
42
+* @access  public
43
+* @version 1.0
44
+* @package Auth_SASL
45
+*/
46
+
47
+class Auth_SASL_Common
48
+{
49
+    /**
50
+    * Function which implements HMAC MD5 digest
51
+    *
52
+    * @param  string $key  The secret key
53
+    * @param  string $data The data to protect
54
+    * @return string       The HMAC MD5 digest
55
+    */
56
+    function _HMAC_MD5($key, $data)
57
+    {
58
+        if (strlen($key) > 64) {
59
+            $key = pack('H32', md5($key));
60
+        }
61
+
62
+        if (strlen($key) < 64) {
63
+            $key = str_pad($key, 64, chr(0));
64
+        }
65
+
66
+        $k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64);
67
+        $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64);
68
+
69
+        $inner  = pack('H32', md5($k_ipad . $data));
70
+        $digest = md5($k_opad . $inner);
71
+
72
+        return $digest;
73
+    }
74
+}
75
+?>
76
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Auth/SASL/CramMD5.php Added
71
 
1
@@ -0,0 +1,68 @@
2
+<?php
3
+// +-----------------------------------------------------------------------+ 
4
+// | Copyright (c) 2002-2003 Richard Heyes                                 | 
5
+// | All rights reserved.                                                  | 
6
+// |                                                                       | 
7
+// | Redistribution and use in source and binary forms, with or without    | 
8
+// | modification, are permitted provided that the following conditions    | 
9
+// | are met:                                                              | 
10
+// |                                                                       | 
11
+// | o Redistributions of source code must retain the above copyright      | 
12
+// |   notice, this list of conditions and the following disclaimer.       | 
13
+// | o Redistributions in binary form must reproduce the above copyright   | 
14
+// |   notice, this list of conditions and the following disclaimer in the | 
15
+// |   documentation and/or other materials provided with the distribution.| 
16
+// | o The names of the authors may not be used to endorse or promote      | 
17
+// |   products derived from this software without specific prior written  | 
18
+// |   permission.                                                         | 
19
+// |                                                                       | 
20
+// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   | 
21
+// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     | 
22
+// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 
23
+// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  | 
24
+// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 
25
+// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      | 
26
+// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 
27
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 
28
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   | 
29
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
30
+// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  | 
31
+// |                                                                       | 
32
+// +-----------------------------------------------------------------------+ 
33
+// | Author: Richard Heyes <richard@php.net>                               | 
34
+// +-----------------------------------------------------------------------+ 
35
+// 
36
+// $Id$
37
+
38
+/**
39
+* Implmentation of CRAM-MD5 SASL mechanism
40
+*
41
+* @author  Richard Heyes <richard@php.net>
42
+* @access  public
43
+* @version 1.0
44
+* @package Auth_SASL
45
+*/
46
+
47
+require_once('Auth/SASL/Common.php');
48
+
49
+class Auth_SASL_CramMD5 extends Auth_SASL_Common
50
+{
51
+    /**
52
+    * Implements the CRAM-MD5 SASL mechanism
53
+    * This DOES NOT base64 encode the return value,
54
+    * you will need to do that yourself.
55
+    *
56
+    * @param string $user      Username
57
+    * @param string $pass      Password
58
+    * @param string $challenge The challenge supplied by the server.
59
+    *                          this should be already base64_decoded.
60
+    *
61
+    * @return string The string to pass back to the server, of the form
62
+    *                "<user> <digest>". This is NOT base64_encoded.
63
+    */
64
+    function getResponse($user, $pass, $challenge)
65
+    {
66
+        return $user . ' ' . $this->_HMAC_MD5($pass, $challenge);
67
+    }
68
+}
69
+?>
70
\ No newline at end of file
71
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Auth/SASL/DigestMD5.php Added
199
 
1
@@ -0,0 +1,197 @@
2
+<?php
3
+// +-----------------------------------------------------------------------+ 
4
+// | Copyright (c) 2002-2003 Richard Heyes                                 | 
5
+// | All rights reserved.                                                  | 
6
+// |                                                                       | 
7
+// | Redistribution and use in source and binary forms, with or without    | 
8
+// | modification, are permitted provided that the following conditions    | 
9
+// | are met:                                                              | 
10
+// |                                                                       | 
11
+// | o Redistributions of source code must retain the above copyright      | 
12
+// |   notice, this list of conditions and the following disclaimer.       | 
13
+// | o Redistributions in binary form must reproduce the above copyright   | 
14
+// |   notice, this list of conditions and the following disclaimer in the | 
15
+// |   documentation and/or other materials provided with the distribution.| 
16
+// | o The names of the authors may not be used to endorse or promote      | 
17
+// |   products derived from this software without specific prior written  | 
18
+// |   permission.                                                         | 
19
+// |                                                                       | 
20
+// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   | 
21
+// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     | 
22
+// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 
23
+// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  | 
24
+// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 
25
+// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      | 
26
+// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 
27
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 
28
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   | 
29
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
30
+// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  | 
31
+// |                                                                       | 
32
+// +-----------------------------------------------------------------------+ 
33
+// | Author: Richard Heyes <richard@php.net>                               | 
34
+// +-----------------------------------------------------------------------+ 
35
+// 
36
+// $Id$
37
+
38
+/**
39
+* Implmentation of DIGEST-MD5 SASL mechanism
40
+*
41
+* @author  Richard Heyes <richard@php.net>
42
+* @access  public
43
+* @version 1.0
44
+* @package Auth_SASL
45
+*/
46
+
47
+require_once('Auth/SASL/Common.php');
48
+
49
+class Auth_SASL_DigestMD5 extends Auth_SASL_Common
50
+{
51
+    /**
52
+    * Provides the (main) client response for DIGEST-MD5
53
+    * requires a few extra parameters than the other
54
+    * mechanisms, which are unavoidable.
55
+    * 
56
+    * @param  string $authcid   Authentication id (username)
57
+    * @param  string $pass      Password
58
+    * @param  string $challenge The digest challenge sent by the server
59
+    * @param  string $hostname  The hostname of the machine you're connecting to
60
+    * @param  string $service   The servicename (eg. imap, pop, acap etc)
61
+    * @param  string $authzid   Authorization id (username to proxy as)
62
+    * @return string            The digest response (NOT base64 encoded)
63
+    * @access public
64
+    */
65
+    function getResponse($authcid, $pass, $challenge, $hostname, $service, $authzid = '')
66
+    {
67
+        $challenge = $this->_parseChallenge($challenge);
68
+        $authzid_string = '';
69
+        if ($authzid != '') {
70
+            $authzid_string = ',authzid="' . $authzid . '"'; 
71
+        }
72
+
73
+        if (!empty($challenge)) {
74
+            $cnonce         = $this->_getCnonce();
75
+            $digest_uri     = sprintf('%s/%s', $service, $hostname);
76
+            $response_value = $this->_getResponseValue($authcid, $pass, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $authzid);
77
+
78
+            if ($challenge['realm']) {
79
+                return sprintf('username="%s",realm="%s"' . $authzid_string  .
80
+',nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,maxbuf=%d', $authcid, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']);
81
+            } else {
82
+                return sprintf('username="%s"' . $authzid_string  . ',nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,maxbuf=%d', $authcid, $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']);
83
+            }
84
+        } else {
85
+            return PEAR::raiseError('Invalid digest challenge');
86
+        }
87
+    }
88
+    
89
+    /**
90
+    * Parses and verifies the digest challenge*
91
+    *
92
+    * @param  string $challenge The digest challenge
93
+    * @return array             The parsed challenge as an assoc
94
+    *                           array in the form "directive => value".
95
+    * @access private
96
+    */
97
+    function _parseChallenge($challenge)
98
+    {
99
+        $tokens = array();
100
+        while (preg_match('/^([a-z-]+)=("[^"]+(?<!\\\)"|[^,]+)/i', $challenge, $matches)) {
101
+
102
+            // Ignore these as per rfc2831
103
+            if ($matches[1] == 'opaque' OR $matches[1] == 'domain') {
104
+                $challenge = substr($challenge, strlen($matches[0]) + 1);
105
+                continue;
106
+            }
107
+
108
+            // Allowed multiple "realm" and "auth-param"
109
+            if (!empty($tokens[$matches[1]]) AND ($matches[1] == 'realm' OR $matches[1] == 'auth-param')) {
110
+                if (is_array($tokens[$matches[1]])) {
111
+                    $tokens[$matches[1]][] = preg_replace('/^"(.*)"$/', '\\1', $matches[2]);
112
+                } else {
113
+                    $tokens[$matches[1]] = array($tokens[$matches[1]], preg_replace('/^"(.*)"$/', '\\1', $matches[2]));
114
+                }
115
+
116
+            // Any other multiple instance = failure
117
+            } elseif (!empty($tokens[$matches[1]])) {
118
+                $tokens = array();
119
+                break;
120
+
121
+            } else {
122
+                $tokens[$matches[1]] = preg_replace('/^"(.*)"$/', '\\1', $matches[2]);
123
+            }
124
+
125
+            // Remove the just parsed directive from the challenge
126
+            $challenge = substr($challenge, strlen($matches[0]) + 1);
127
+        }
128
+
129
+        /**
130
+        * Defaults and required directives
131
+        */
132
+        // Realm
133
+        if (empty($tokens['realm'])) {
134
+            $tokens['realm'] = "";
135
+        }
136
+
137
+        // Maxbuf
138
+        if (empty($tokens['maxbuf'])) {
139
+            $tokens['maxbuf'] = 65536;
140
+        }
141
+
142
+        // Required: nonce, algorithm
143
+        if (empty($tokens['nonce']) OR empty($tokens['algorithm'])) {
144
+            return array();
145
+        }
146
+
147
+        return $tokens;
148
+    }
149
+
150
+    /**
151
+    * Creates the response= part of the digest response
152
+    *
153
+    * @param  string $authcid    Authentication id (username)
154
+    * @param  string $pass       Password
155
+    * @param  string $realm      Realm as provided by the server
156
+    * @param  string $nonce      Nonce as provided by the server
157
+    * @param  string $cnonce     Client nonce
158
+    * @param  string $digest_uri The digest-uri= value part of the response
159
+    * @param  string $authzid    Authorization id
160
+    * @return string             The response= part of the digest response
161
+    * @access private
162
+    */    
163
+    function _getResponseValue($authcid, $pass, $realm, $nonce, $cnonce, $digest_uri, $authzid = '')
164
+    {
165
+        if ($authzid == '') {
166
+            $A1 = sprintf('%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $authcid, $realm, $pass))), $nonce, $cnonce);
167
+        } else {
168
+            $A1 = sprintf('%s:%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $authcid, $realm, $pass))), $nonce, $cnonce, $authzid);
169
+        }
170
+        $A2 = 'AUTHENTICATE:' . $digest_uri;
171
+        return md5(sprintf('%s:%s:00000001:%s:auth:%s', md5($A1), $nonce, $cnonce, md5($A2)));
172
+    }
173
+
174
+    /**
175
+    * Creates the client nonce for the response
176
+    *
177
+    * @return string  The cnonce value
178
+    * @access private
179
+    */
180
+    function _getCnonce()
181
+    {
182
+        if (@file_exists('/dev/urandom') && $fd = @fopen('/dev/urandom', 'r')) {
183
+            return base64_encode(fread($fd, 32));
184
+
185
+        } elseif (@file_exists('/dev/random') && $fd = @fopen('/dev/random', 'r')) {
186
+            return base64_encode(fread($fd, 32));
187
+
188
+        } else {
189
+            $str = '';
190
+            for ($i=0; $i<32; $i++) {
191
+                $str .= chr(mt_rand(0, 255));
192
+            }
193
+            
194
+            return base64_encode($str);
195
+        }
196
+    }
197
+}
198
+?>
199
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Auth/SASL/External.php Added
65
 
1
@@ -0,0 +1,63 @@
2
+<?php
3
+// +-----------------------------------------------------------------------+ 
4
+// | Copyright (c) 2008 Christoph Schulz                                   | 
5
+// | All rights reserved.                                                  | 
6
+// |                                                                       | 
7
+// | Redistribution and use in source and binary forms, with or without    | 
8
+// | modification, are permitted provided that the following conditions    | 
9
+// | are met:                                                              | 
10
+// |                                                                       | 
11
+// | o Redistributions of source code must retain the above copyright      | 
12
+// |   notice, this list of conditions and the following disclaimer.       | 
13
+// | o Redistributions in binary form must reproduce the above copyright   | 
14
+// |   notice, this list of conditions and the following disclaimer in the | 
15
+// |   documentation and/or other materials provided with the distribution.| 
16
+// | o The names of the authors may not be used to endorse or promote      | 
17
+// |   products derived from this software without specific prior written  | 
18
+// |   permission.                                                         | 
19
+// |                                                                       | 
20
+// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   | 
21
+// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     | 
22
+// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 
23
+// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  | 
24
+// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 
25
+// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      | 
26
+// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 
27
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 
28
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   | 
29
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
30
+// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  | 
31
+// |                                                                       | 
32
+// +-----------------------------------------------------------------------+ 
33
+// | Author: Christoph Schulz <develop@kristov.de>                         | 
34
+// +-----------------------------------------------------------------------+ 
35
+// 
36
+// $Id: External.php 286825 2009-08-05 06:23:42Z cweiske $
37
+
38
+/**
39
+* Implmentation of EXTERNAL SASL mechanism
40
+*
41
+* @author  Christoph Schulz <develop@kristov.de>
42
+* @access  public
43
+* @version 1.0.3
44
+* @package Auth_SASL
45
+*/
46
+
47
+require_once('Auth/SASL/Common.php');
48
+
49
+class Auth_SASL_External extends Auth_SASL_Common
50
+{
51
+    /**
52
+    * Returns EXTERNAL response
53
+    *
54
+    * @param  string $authcid   Authentication id (username)
55
+    * @param  string $pass      Password
56
+    * @param  string $authzid   Autorization id
57
+    * @return string            EXTERNAL Response
58
+    */
59
+    function getResponse($authcid, $pass, $authzid = '')
60
+    {
61
+        return $authzid;
62
+    }
63
+}
64
+?>
65
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Auth/SASL/Login.php Added
68
 
1
@@ -0,0 +1,65 @@
2
+<?php
3
+// +-----------------------------------------------------------------------+ 
4
+// | Copyright (c) 2002-2003 Richard Heyes                                 | 
5
+// | All rights reserved.                                                  | 
6
+// |                                                                       | 
7
+// | Redistribution and use in source and binary forms, with or without    | 
8
+// | modification, are permitted provided that the following conditions    | 
9
+// | are met:                                                              | 
10
+// |                                                                       | 
11
+// | o Redistributions of source code must retain the above copyright      | 
12
+// |   notice, this list of conditions and the following disclaimer.       | 
13
+// | o Redistributions in binary form must reproduce the above copyright   | 
14
+// |   notice, this list of conditions and the following disclaimer in the | 
15
+// |   documentation and/or other materials provided with the distribution.| 
16
+// | o The names of the authors may not be used to endorse or promote      | 
17
+// |   products derived from this software without specific prior written  | 
18
+// |   permission.                                                         | 
19
+// |                                                                       | 
20
+// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   | 
21
+// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     | 
22
+// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 
23
+// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  | 
24
+// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 
25
+// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      | 
26
+// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 
27
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 
28
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   | 
29
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
30
+// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  | 
31
+// |                                                                       | 
32
+// +-----------------------------------------------------------------------+ 
33
+// | Author: Richard Heyes <richard@php.net>                               | 
34
+// +-----------------------------------------------------------------------+ 
35
+// 
36
+// $Id$
37
+
38
+/**
39
+* This is technically not a SASL mechanism, however
40
+* it's used by Net_Sieve, Net_Cyrus and potentially
41
+* other protocols , so here is a good place to abstract
42
+* it.
43
+*
44
+* @author  Richard Heyes <richard@php.net>
45
+* @access  public
46
+* @version 1.0
47
+* @package Auth_SASL
48
+*/
49
+
50
+require_once('Auth/SASL/Common.php');
51
+
52
+class Auth_SASL_Login extends Auth_SASL_Common
53
+{
54
+    /**
55
+    * Pseudo SASL LOGIN mechanism
56
+    *
57
+    * @param  string $user Username
58
+    * @param  string $pass Password
59
+    * @return string       LOGIN string
60
+    */
61
+    function getResponse($user, $pass)
62
+    {
63
+        return sprintf('LOGIN %s %s', $user, $pass);
64
+    }
65
+}
66
+?>
67
\ No newline at end of file
68
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Auth/SASL/Plain.php Added
65
 
1
@@ -0,0 +1,63 @@
2
+<?php
3
+// +-----------------------------------------------------------------------+ 
4
+// | Copyright (c) 2002-2003 Richard Heyes                                 | 
5
+// | All rights reserved.                                                  | 
6
+// |                                                                       | 
7
+// | Redistribution and use in source and binary forms, with or without    | 
8
+// | modification, are permitted provided that the following conditions    | 
9
+// | are met:                                                              | 
10
+// |                                                                       | 
11
+// | o Redistributions of source code must retain the above copyright      | 
12
+// |   notice, this list of conditions and the following disclaimer.       | 
13
+// | o Redistributions in binary form must reproduce the above copyright   | 
14
+// |   notice, this list of conditions and the following disclaimer in the | 
15
+// |   documentation and/or other materials provided with the distribution.| 
16
+// | o The names of the authors may not be used to endorse or promote      | 
17
+// |   products derived from this software without specific prior written  | 
18
+// |   permission.                                                         | 
19
+// |                                                                       | 
20
+// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   | 
21
+// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     | 
22
+// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 
23
+// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  | 
24
+// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 
25
+// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      | 
26
+// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 
27
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 
28
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   | 
29
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
30
+// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  | 
31
+// |                                                                       | 
32
+// +-----------------------------------------------------------------------+ 
33
+// | Author: Richard Heyes <richard@php.net>                               | 
34
+// +-----------------------------------------------------------------------+ 
35
+// 
36
+// $Id$
37
+
38
+/**
39
+* Implmentation of PLAIN SASL mechanism
40
+*
41
+* @author  Richard Heyes <richard@php.net>
42
+* @access  public
43
+* @version 1.0
44
+* @package Auth_SASL
45
+*/
46
+
47
+require_once('Auth/SASL/Common.php');
48
+
49
+class Auth_SASL_Plain extends Auth_SASL_Common
50
+{
51
+    /**
52
+    * Returns PLAIN response
53
+    *
54
+    * @param  string $authcid   Authentication id (username)
55
+    * @param  string $pass      Password
56
+    * @param  string $authzid   Autorization id
57
+    * @return string            PLAIN Response
58
+    */
59
+    function getResponse($authcid, $pass, $authzid = '')
60
+    {
61
+        return $authzid . chr(0) . $authcid . chr(0) . $pass;
62
+    }
63
+}
64
+?>
65
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2 Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2.php Added
1017
 
1
@@ -0,0 +1,1015 @@
2
+<?php
3
+/**
4
+ * Class representing a HTTP request message
5
+ *
6
+ * PHP version 5
7
+ *
8
+ * LICENSE:
9
+ *
10
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>
11
+ * All rights reserved.
12
+ *
13
+ * Redistribution and use in source and binary forms, with or without
14
+ * modification, are permitted provided that the following conditions
15
+ * are met:
16
+ *
17
+ *    * Redistributions of source code must retain the above copyright
18
+ *      notice, this list of conditions and the following disclaimer.
19
+ *    * Redistributions in binary form must reproduce the above copyright
20
+ *      notice, this list of conditions and the following disclaimer in the
21
+ *      documentation and/or other materials provided with the distribution.
22
+ *    * The names of the authors may not be used to endorse or promote products
23
+ *      derived from this software without specific prior written permission.
24
+ *
25
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ *
37
+ * @category   HTTP
38
+ * @package    HTTP_Request2
39
+ * @author     Alexey Borzov <avb@php.net>
40
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
41
+ * @version    SVN: $Id: Request2.php 315409 2011-08-24 07:29:23Z avb $
42
+ * @link       http://pear.php.net/package/HTTP_Request2
43
+ */
44
+
45
+/**
46
+ * A class representing an URL as per RFC 3986.
47
+ */
48
+require_once 'Net/URL2.php';
49
+
50
+/**
51
+ * Exception class for HTTP_Request2 package
52
+ */
53
+require_once 'HTTP/Request2/Exception.php';
54
+
55
+/**
56
+ * Class representing a HTTP request message
57
+ *
58
+ * @category   HTTP
59
+ * @package    HTTP_Request2
60
+ * @author     Alexey Borzov <avb@php.net>
61
+ * @version    Release: 2.0.0
62
+ * @link       http://tools.ietf.org/html/rfc2616#section-5
63
+ */
64
+class HTTP_Request2 implements SplSubject
65
+{
66
+   /**#@+
67
+    * Constants for HTTP request methods
68
+    *
69
+    * @link http://tools.ietf.org/html/rfc2616#section-5.1.1
70
+    */
71
+    const METHOD_OPTIONS = 'OPTIONS';
72
+    const METHOD_GET     = 'GET';
73
+    const METHOD_HEAD    = 'HEAD';
74
+    const METHOD_POST    = 'POST';
75
+    const METHOD_PUT     = 'PUT';
76
+    const METHOD_DELETE  = 'DELETE';
77
+    const METHOD_TRACE   = 'TRACE';
78
+    const METHOD_CONNECT = 'CONNECT';
79
+   /**#@-*/
80
+
81
+   /**#@+
82
+    * Constants for HTTP authentication schemes
83
+    *
84
+    * @link http://tools.ietf.org/html/rfc2617
85
+    */
86
+    const AUTH_BASIC  = 'basic';
87
+    const AUTH_DIGEST = 'digest';
88
+   /**#@-*/
89
+
90
+   /**
91
+    * Regular expression used to check for invalid symbols in RFC 2616 tokens
92
+    * @link http://pear.php.net/bugs/bug.php?id=15630
93
+    */
94
+    const REGEXP_INVALID_TOKEN = '![\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]!';
95
+
96
+   /**
97
+    * Regular expression used to check for invalid symbols in cookie strings
98
+    * @link http://pear.php.net/bugs/bug.php?id=15630
99
+    * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html
100
+    */
101
+    const REGEXP_INVALID_COOKIE = '/[\s,;]/';
102
+
103
+   /**
104
+    * Fileinfo magic database resource
105
+    * @var  resource
106
+    * @see  detectMimeType()
107
+    */
108
+    private static $_fileinfoDb;
109
+
110
+   /**
111
+    * Observers attached to the request (instances of SplObserver)
112
+    * @var  array
113
+    */
114
+    protected $observers = array();
115
+
116
+   /**
117
+    * Request URL
118
+    * @var  Net_URL2
119
+    */
120
+    protected $url;
121
+
122
+   /**
123
+    * Request method
124
+    * @var  string
125
+    */
126
+    protected $method = self::METHOD_GET;
127
+
128
+   /**
129
+    * Authentication data
130
+    * @var  array
131
+    * @see  getAuth()
132
+    */
133
+    protected $auth;
134
+
135
+   /**
136
+    * Request headers
137
+    * @var  array
138
+    */
139
+    protected $headers = array();
140
+
141
+   /**
142
+    * Configuration parameters
143
+    * @var  array
144
+    * @see  setConfig()
145
+    */
146
+    protected $config = array(
147
+        'adapter'           => 'HTTP_Request2_Adapter_Socket',
148
+        'connect_timeout'   => 10,
149
+        'timeout'           => 0,
150
+        'use_brackets'      => true,
151
+        'protocol_version'  => '1.1',
152
+        'buffer_size'       => 16384,
153
+        'store_body'        => true,
154
+
155
+        'proxy_host'        => '',
156
+        'proxy_port'        => '',
157
+        'proxy_user'        => '',
158
+        'proxy_password'    => '',
159
+        'proxy_auth_scheme' => self::AUTH_BASIC,
160
+
161
+        'ssl_verify_peer'   => true,
162
+        'ssl_verify_host'   => true,
163
+        'ssl_cafile'        => null,
164
+        'ssl_capath'        => null,
165
+        'ssl_local_cert'    => null,
166
+        'ssl_passphrase'    => null,
167
+
168
+        'digest_compat_ie'  => false,
169
+
170
+        'follow_redirects'  => false,
171
+        'max_redirects'     => 5,
172
+        'strict_redirects'  => false
173
+    );
174
+
175
+   /**
176
+    * Last event in request / response handling, intended for observers
177
+    * @var  array
178
+    * @see  getLastEvent()
179
+    */
180
+    protected $lastEvent = array(
181
+        'name' => 'start',
182
+        'data' => null
183
+    );
184
+
185
+   /**
186
+    * Request body
187
+    * @var  string|resource
188
+    * @see  setBody()
189
+    */
190
+    protected $body = '';
191
+
192
+   /**
193
+    * Array of POST parameters
194
+    * @var  array
195
+    */
196
+    protected $postParams = array();
197
+
198
+   /**
199
+    * Array of file uploads (for multipart/form-data POST requests)
200
+    * @var  array
201
+    */
202
+    protected $uploads = array();
203
+
204
+   /**
205
+    * Adapter used to perform actual HTTP request
206
+    * @var  HTTP_Request2_Adapter
207
+    */
208
+    protected $adapter;
209
+
210
+   /**
211
+    * Cookie jar to persist cookies between requests
212
+    * @var HTTP_Request2_CookieJar
213
+    */
214
+    protected $cookieJar = null;
215
+
216
+   /**
217
+    * Constructor. Can set request URL, method and configuration array.
218
+    *
219
+    * Also sets a default value for User-Agent header.
220
+    *
221
+    * @param    string|Net_Url2     Request URL
222
+    * @param    string              Request method
223
+    * @param    array               Configuration for this Request instance
224
+    */
225
+    public function __construct($url = null, $method = self::METHOD_GET, array $config = array())
226
+    {
227
+        $this->setConfig($config);
228
+        if (!empty($url)) {
229
+            $this->setUrl($url);
230
+        }
231
+        if (!empty($method)) {
232
+            $this->setMethod($method);
233
+        }
234
+        $this->setHeader('user-agent', 'HTTP_Request2/2.0.0 ' .
235
+                         '(http://pear.php.net/package/http_request2) ' .
236
+                         'PHP/' . phpversion());
237
+    }
238
+
239
+   /**
240
+    * Sets the URL for this request
241
+    *
242
+    * If the URL has userinfo part (username & password) these will be removed
243
+    * and converted to auth data. If the URL does not have a path component,
244
+    * that will be set to '/'.
245
+    *
246
+    * @param    string|Net_URL2 Request URL
247
+    * @return   HTTP_Request2
248
+    * @throws   HTTP_Request2_LogicException
249
+    */
250
+    public function setUrl($url)
251
+    {
252
+        if (is_string($url)) {
253
+            $url = new Net_URL2(
254
+                $url, array(Net_URL2::OPTION_USE_BRACKETS => $this->config['use_brackets'])
255
+            );
256
+        }
257
+        if (!$url instanceof Net_URL2) {
258
+            throw new HTTP_Request2_LogicException(
259
+                'Parameter is not a valid HTTP URL',
260
+                HTTP_Request2_Exception::INVALID_ARGUMENT
261
+            );
262
+        }
263
+        // URL contains username / password?
264
+        if ($url->getUserinfo()) {
265
+            $username = $url->getUser();
266
+            $password = $url->getPassword();
267
+            $this->setAuth(rawurldecode($username), $password? rawurldecode($password): '');
268
+            $url->setUserinfo('');
269
+        }
270
+        if ('' == $url->getPath()) {
271
+            $url->setPath('/');
272
+        }
273
+        $this->url = $url;
274
+
275
+        return $this;
276
+    }
277
+
278
+   /**
279
+    * Returns the request URL
280
+    *
281
+    * @return   Net_URL2
282
+    */
283
+    public function getUrl()
284
+    {
285
+        return $this->url;
286
+    }
287
+
288
+   /**
289
+    * Sets the request method
290
+    *
291
+    * @param    string
292
+    * @return   HTTP_Request2
293
+    * @throws   HTTP_Request2_LogicException if the method name is invalid
294
+    */
295
+    public function setMethod($method)
296
+    {
297
+        // Method name should be a token: http://tools.ietf.org/html/rfc2616#section-5.1.1
298
+        if (preg_match(self::REGEXP_INVALID_TOKEN, $method)) {
299
+            throw new HTTP_Request2_LogicException(
300
+                "Invalid request method '{$method}'",
301
+                HTTP_Request2_Exception::INVALID_ARGUMENT
302
+            );
303
+        }
304
+        $this->method = $method;
305
+
306
+        return $this;
307
+    }
308
+
309
+   /**
310
+    * Returns the request method
311
+    *
312
+    * @return   string
313
+    */
314
+    public function getMethod()
315
+    {
316
+        return $this->method;
317
+    }
318
+
319
+   /**
320
+    * Sets the configuration parameter(s)
321
+    *
322
+    * The following parameters are available:
323
+    * <ul>
324
+    *   <li> 'adapter'           - adapter to use (string)</li>
325
+    *   <li> 'connect_timeout'   - Connection timeout in seconds (integer)</li>
326
+    *   <li> 'timeout'           - Total number of seconds a request can take.
327
+    *                              Use 0 for no limit, should be greater than
328
+    *                              'connect_timeout' if set (integer)</li>
329
+    *   <li> 'use_brackets'      - Whether to append [] to array variable names (bool)</li>
330
+    *   <li> 'protocol_version'  - HTTP Version to use, '1.0' or '1.1' (string)</li>
331
+    *   <li> 'buffer_size'       - Buffer size to use for reading and writing (int)</li>
332
+    *   <li> 'store_body'        - Whether to store response body in response object.
333
+    *                              Set to false if receiving a huge response and
334
+    *                              using an Observer to save it (boolean)</li>
335
+    *   <li> 'proxy_host'        - Proxy server host (string)</li>
336
+    *   <li> 'proxy_port'        - Proxy server port (integer)</li>
337
+    *   <li> 'proxy_user'        - Proxy auth username (string)</li>
338
+    *   <li> 'proxy_password'    - Proxy auth password (string)</li>
339
+    *   <li> 'proxy_auth_scheme' - Proxy auth scheme, one of HTTP_Request2::AUTH_* constants (string)</li>
340
+    *   <li> 'ssl_verify_peer'   - Whether to verify peer's SSL certificate (bool)</li>
341
+    *   <li> 'ssl_verify_host'   - Whether to check that Common Name in SSL
342
+    *                              certificate matches host name (bool)</li>
343
+    *   <li> 'ssl_cafile'        - Cerificate Authority file to verify the peer
344
+    *                              with (use with 'ssl_verify_peer') (string)</li>
345
+    *   <li> 'ssl_capath'        - Directory holding multiple Certificate
346
+    *                              Authority files (string)</li>
347
+    *   <li> 'ssl_local_cert'    - Name of a file containing local cerificate (string)</li>
348
+    *   <li> 'ssl_passphrase'    - Passphrase with which local certificate
349
+    *                              was encoded (string)</li>
350
+    *   <li> 'digest_compat_ie'  - Whether to imitate behaviour of MSIE 5 and 6
351
+    *                              in using URL without query string in digest
352
+    *                              authentication (boolean)</li>
353
+    *   <li> 'follow_redirects'  - Whether to automatically follow HTTP Redirects (boolean)</li>
354
+    *   <li> 'max_redirects'     - Maximum number of redirects to follow (integer)</li>
355
+    *   <li> 'strict_redirects'  - Whether to keep request method on redirects via status 301 and
356
+    *                              302 (true, needed for compatibility with RFC 2616)
357
+    *                              or switch to GET (false, needed for compatibility with most
358
+    *                              browsers) (boolean)</li>
359
+    * </ul>
360
+    *
361
+    * @param    string|array    configuration parameter name or array
362
+    *                           ('parameter name' => 'parameter value')
363
+    * @param    mixed           parameter value if $nameOrConfig is not an array
364
+    * @return   HTTP_Request2
365
+    * @throws   HTTP_Request2_LogicException If the parameter is unknown
366
+    */
367
+    public function setConfig($nameOrConfig, $value = null)
368
+    {
369
+        if (is_array($nameOrConfig)) {
370
+            foreach ($nameOrConfig as $name => $value) {
371
+                $this->setConfig($name, $value);
372
+            }
373
+
374
+        } else {
375
+            if (!array_key_exists($nameOrConfig, $this->config)) {
376
+                throw new HTTP_Request2_LogicException(
377
+                    "Unknown configuration parameter '{$nameOrConfig}'",
378
+                    HTTP_Request2_Exception::INVALID_ARGUMENT
379
+                );
380
+            }
381
+            $this->config[$nameOrConfig] = $value;
382
+        }
383
+
384
+        return $this;
385
+    }
386
+
387
+   /**
388
+    * Returns the value(s) of the configuration parameter(s)
389
+    *
390
+    * @param    string  parameter name
391
+    * @return   mixed   value of $name parameter, array of all configuration
392
+    *                   parameters if $name is not given
393
+    * @throws   HTTP_Request2_LogicException If the parameter is unknown
394
+    */
395
+    public function getConfig($name = null)
396
+    {
397
+        if (null === $name) {
398
+            return $this->config;
399
+        } elseif (!array_key_exists($name, $this->config)) {
400
+            throw new HTTP_Request2_LogicException(
401
+                "Unknown configuration parameter '{$name}'",
402
+                HTTP_Request2_Exception::INVALID_ARGUMENT
403
+            );
404
+        }
405
+        return $this->config[$name];
406
+    }
407
+
408
+   /**
409
+    * Sets the autentification data
410
+    *
411
+    * @param    string  user name
412
+    * @param    string  password
413
+    * @param    string  authentication scheme
414
+    * @return   HTTP_Request2
415
+    */
416
+    public function setAuth($user, $password = '', $scheme = self::AUTH_BASIC)
417
+    {
418
+        if (empty($user)) {
419
+            $this->auth = null;
420
+        } else {
421
+            $this->auth = array(
422
+                'user'     => (string)$user,
423
+                'password' => (string)$password,
424
+                'scheme'   => $scheme
425
+            );
426
+        }
427
+
428
+        return $this;
429
+    }
430
+
431
+   /**
432
+    * Returns the authentication data
433
+    *
434
+    * The array has the keys 'user', 'password' and 'scheme', where 'scheme'
435
+    * is one of the HTTP_Request2::AUTH_* constants.
436
+    *
437
+    * @return   array
438
+    */
439
+    public function getAuth()
440
+    {
441
+        return $this->auth;
442
+    }
443
+
444
+   /**
445
+    * Sets request header(s)
446
+    *
447
+    * The first parameter may be either a full header string 'header: value' or
448
+    * header name. In the former case $value parameter is ignored, in the latter
449
+    * the header's value will either be set to $value or the header will be
450
+    * removed if $value is null. The first parameter can also be an array of
451
+    * headers, in that case method will be called recursively.
452
+    *
453
+    * Note that headers are treated case insensitively as per RFC 2616.
454
+    *
455
+    * <code>
456
+    * $req->setHeader('Foo: Bar'); // sets the value of 'Foo' header to 'Bar'
457
+    * $req->setHeader('FoO', 'Baz'); // sets the value of 'Foo' header to 'Baz'
458
+    * $req->setHeader(array('foo' => 'Quux')); // sets the value of 'Foo' header to 'Quux'
459
+    * $req->setHeader('FOO'); // removes 'Foo' header from request
460
+    * </code>
461
+    *
462
+    * @param    string|array    header name, header string ('Header: value')
463
+    *                           or an array of headers
464
+    * @param    string|array|null header value if $name is not an array,
465
+    *                           header will be removed if value is null
466
+    * @param    bool            whether to replace previous header with the
467
+    *                           same name or append to its value
468
+    * @return   HTTP_Request2
469
+    * @throws   HTTP_Request2_LogicException
470
+    */
471
+    public function setHeader($name, $value = null, $replace = true)
472
+    {
473
+        if (is_array($name)) {
474
+            foreach ($name as $k => $v) {
475
+                if (is_string($k)) {
476
+                    $this->setHeader($k, $v, $replace);
477
+                } else {
478
+                    $this->setHeader($v, null, $replace);
479
+                }
480
+            }
481
+        } else {
482
+            if (null === $value && strpos($name, ':')) {
483
+                list($name, $value) = array_map('trim', explode(':', $name, 2));
484
+            }
485
+            // Header name should be a token: http://tools.ietf.org/html/rfc2616#section-4.2
486
+            if (preg_match(self::REGEXP_INVALID_TOKEN, $name)) {
487
+                throw new HTTP_Request2_LogicException(
488
+                    "Invalid header name '{$name}'",
489
+                    HTTP_Request2_Exception::INVALID_ARGUMENT
490
+                );
491
+            }
492
+            // Header names are case insensitive anyway
493
+            $name = strtolower($name);
494
+            if (null === $value) {
495
+                unset($this->headers[$name]);
496
+
497
+            } else {
498
+                if (is_array($value)) {
499
+                    $value = implode(', ', array_map('trim', $value));
500
+                } elseif (is_string($value)) {
501
+                    $value = trim($value);
502
+                }
503
+                if (!isset($this->headers[$name]) || $replace) {
504
+                    $this->headers[$name] = $value;
505
+                } else {
506
+                    $this->headers[$name] .= ', ' . $value;
507
+                }
508
+            }
509
+        }
510
+
511
+        return $this;
512
+    }
513
+
514
+   /**
515
+    * Returns the request headers
516
+    *
517
+    * The array is of the form ('header name' => 'header value'), header names
518
+    * are lowercased
519
+    *
520
+    * @return   array
521
+    */
522
+    public function getHeaders()
523
+    {
524
+        return $this->headers;
525
+    }
526
+
527
+   /**
528
+    * Adds a cookie to the request
529
+    *
530
+    * If the request does not have a CookieJar object set, this method simply
531
+    * appends a cookie to "Cookie:" header.
532
+    *
533
+    * If a CookieJar object is available, the cookie is stored in that object.
534
+    * Data from request URL will be used for setting its 'domain' and 'path'
535
+    * parameters, 'expires' and 'secure' will be set to null and false,
536
+    * respectively. If you need further control, use CookieJar's methods.
537
+    *
538
+    * @param    string  cookie name
539
+    * @param    string  cookie value
540
+    * @return   HTTP_Request2
541
+    * @throws   HTTP_Request2_LogicException
542
+    * @see      setCookieJar()
543
+    */
544
+    public function addCookie($name, $value)
545
+    {
546
+        if (!empty($this->cookieJar)) {
547
+            $this->cookieJar->store(array('name' => $name, 'value' => $value),
548
+                                    $this->url);
549
+
550
+        } else {
551
+            $cookie = $name . '=' . $value;
552
+            if (preg_match(self::REGEXP_INVALID_COOKIE, $cookie)) {
553
+                throw new HTTP_Request2_LogicException(
554
+                    "Invalid cookie: '{$cookie}'",
555
+                    HTTP_Request2_Exception::INVALID_ARGUMENT
556
+                );
557
+            }
558
+            $cookies = empty($this->headers['cookie'])? '': $this->headers['cookie'] . '; ';
559
+            $this->setHeader('cookie', $cookies . $cookie);
560
+        }
561
+
562
+        return $this;
563
+    }
564
+
565
+   /**
566
+    * Sets the request body
567
+    *
568
+    * If you provide file pointer rather than file name, it should support
569
+    * fstat() and rewind() operations.
570
+    *
571
+    * @param    string|resource|HTTP_Request2_MultipartBody  Either a string
572
+    *               with the body or filename containing body or pointer to
573
+    *               an open file or object with multipart body data
574
+    * @param    bool    Whether first parameter is a filename
575
+    * @return   HTTP_Request2
576
+    * @throws   HTTP_Request2_LogicException
577
+    */
578
+    public function setBody($body, $isFilename = false)
579
+    {
580
+        if (!$isFilename && !is_resource($body)) {
581
+            if (!$body instanceof HTTP_Request2_MultipartBody) {
582
+                $this->body = (string)$body;
583
+            } else {
584
+                $this->body = $body;
585
+            }
586
+        } else {
587
+            $fileData = $this->fopenWrapper($body, empty($this->headers['content-type']));
588
+            $this->body = $fileData['fp'];
589
+            if (empty($this->headers['content-type'])) {
590
+                $this->setHeader('content-type', $fileData['type']);
591
+            }
592
+        }
593
+        $this->postParams = $this->uploads = array();
594
+
595
+        return $this;
596
+    }
597
+
598
+   /**
599
+    * Returns the request body
600
+    *
601
+    * @return   string|resource|HTTP_Request2_MultipartBody
602
+    */
603
+    public function getBody()
604
+    {
605
+        if (self::METHOD_POST == $this->method &&
606
+            (!empty($this->postParams) || !empty($this->uploads))
607
+        ) {
608
+            if (0 === strpos($this->headers['content-type'], 'application/x-www-form-urlencoded')) {
609
+                $body = http_build_query($this->postParams, '', '&');
610
+                if (!$this->getConfig('use_brackets')) {
611
+                    $body = preg_replace('/%5B\d+%5D=/', '=', $body);
612
+                }
613
+                // support RFC 3986 by not encoding '~' symbol (request #15368)
614
+                return str_replace('%7E', '~', $body);
615
+
616
+            } elseif (0 === strpos($this->headers['content-type'], 'multipart/form-data')) {
617
+                require_once 'HTTP/Request2/MultipartBody.php';
618
+                return new HTTP_Request2_MultipartBody(
619
+                    $this->postParams, $this->uploads, $this->getConfig('use_brackets')
620
+                );
621
+            }
622
+        }
623
+        return $this->body;
624
+    }
625
+
626
+   /**
627
+    * Adds a file to form-based file upload
628
+    *
629
+    * Used to emulate file upload via a HTML form. The method also sets
630
+    * Content-Type of HTTP request to 'multipart/form-data'.
631
+    *
632
+    * If you just want to send the contents of a file as the body of HTTP
633
+    * request you should use setBody() method.
634
+    *
635
+    * If you provide file pointers rather than file names, they should support
636
+    * fstat() and rewind() operations.
637
+    *
638
+    * @param    string  name of file-upload field
639
+    * @param    string|resource|array   full name of local file, pointer to
640
+    *               open file or an array of files
641
+    * @param    string  filename to send in the request
642
+    * @param    string  content-type of file being uploaded
643
+    * @return   HTTP_Request2
644
+    * @throws   HTTP_Request2_LogicException
645
+    */
646
+    public function addUpload($fieldName, $filename, $sendFilename = null,
647
+                              $contentType = null)
648
+    {
649
+        if (!is_array($filename)) {
650
+            $fileData = $this->fopenWrapper($filename, empty($contentType));
651
+            $this->uploads[$fieldName] = array(
652
+                'fp'        => $fileData['fp'],
653
+                'filename'  => !empty($sendFilename)? $sendFilename
654
+                                :(is_string($filename)? basename($filename): 'anonymous.blob') ,
655
+                'size'      => $fileData['size'],
656
+                'type'      => empty($contentType)? $fileData['type']: $contentType
657
+            );
658
+        } else {
659
+            $fps = $names = $sizes = $types = array();
660
+            foreach ($filename as $f) {
661
+                if (!is_array($f)) {
662
+                    $f = array($f);
663
+                }
664
+                $fileData = $this->fopenWrapper($f[0], empty($f[2]));
665
+                $fps[]   = $fileData['fp'];
666
+                $names[] = !empty($f[1])? $f[1]
667
+                            :(is_string($f[0])? basename($f[0]): 'anonymous.blob');
668
+                $sizes[] = $fileData['size'];
669
+                $types[] = empty($f[2])? $fileData['type']: $f[2];
670
+            }
671
+            $this->uploads[$fieldName] = array(
672
+                'fp' => $fps, 'filename' => $names, 'size' => $sizes, 'type' => $types
673
+            );
674
+        }
675
+        if (empty($this->headers['content-type']) ||
676
+            'application/x-www-form-urlencoded' == $this->headers['content-type']
677
+        ) {
678
+            $this->setHeader('content-type', 'multipart/form-data');
679
+        }
680
+
681
+        return $this;
682
+    }
683
+
684
+   /**
685
+    * Adds POST parameter(s) to the request.
686
+    *
687
+    * @param    string|array    parameter name or array ('name' => 'value')
688
+    * @param    mixed           parameter value (can be an array)
689
+    * @return   HTTP_Request2
690
+    */
691
+    public function addPostParameter($name, $value = null)
692
+    {
693
+        if (!is_array($name)) {
694
+            $this->postParams[$name] = $value;
695
+        } else {
696
+            foreach ($name as $k => $v) {
697
+                $this->addPostParameter($k, $v);
698
+            }
699
+        }
700
+        if (empty($this->headers['content-type'])) {
701
+            $this->setHeader('content-type', 'application/x-www-form-urlencoded');
702
+        }
703
+
704
+        return $this;
705
+    }
706
+
707
+   /**
708
+    * Attaches a new observer
709
+    *
710
+    * @param    SplObserver
711
+    */
712
+    public function attach(SplObserver $observer)
713
+    {
714
+        foreach ($this->observers as $attached) {
715
+            if ($attached === $observer) {
716
+                return;
717
+            }
718
+        }
719
+        $this->observers[] = $observer;
720
+    }
721
+
722
+   /**
723
+    * Detaches an existing observer
724
+    *
725
+    * @param    SplObserver
726
+    */
727
+    public function detach(SplObserver $observer)
728
+    {
729
+        foreach ($this->observers as $key => $attached) {
730
+            if ($attached === $observer) {
731
+                unset($this->observers[$key]);
732
+                return;
733
+            }
734
+        }
735
+    }
736
+
737
+   /**
738
+    * Notifies all observers
739
+    */
740
+    public function notify()
741
+    {
742
+        foreach ($this->observers as $observer) {
743
+            $observer->update($this);
744
+        }
745
+    }
746
+
747
+   /**
748
+    * Sets the last event
749
+    *
750
+    * Adapters should use this method to set the current state of the request
751
+    * and notify the observers.
752
+    *
753
+    * @param    string  event name
754
+    * @param    mixed   event data
755
+    */
756
+    public function setLastEvent($name, $data = null)
757
+    {
758
+        $this->lastEvent = array(
759
+            'name' => $name,
760
+            'data' => $data
761
+        );
762
+        $this->notify();
763
+    }
764
+
765
+   /**
766
+    * Returns the last event
767
+    *
768
+    * Observers should use this method to access the last change in request.
769
+    * The following event names are possible:
770
+    * <ul>
771
+    *   <li>'connect'                 - after connection to remote server,
772
+    *                                   data is the destination (string)</li>
773
+    *   <li>'disconnect'              - after disconnection from server</li>
774
+    *   <li>'sentHeaders'             - after sending the request headers,
775
+    *                                   data is the headers sent (string)</li>
776
+    *   <li>'sentBodyPart'            - after sending a part of the request body,
777
+    *                                   data is the length of that part (int)</li>
778
+    *   <li>'sentBody'                - after sending the whole request body,
779
+    *                                   data is request body length (int)</li>
780
+    *   <li>'receivedHeaders'         - after receiving the response headers,
781
+    *                                   data is HTTP_Request2_Response object</li>
782
+    *   <li>'receivedBodyPart'        - after receiving a part of the response
783
+    *                                   body, data is that part (string)</li>
784
+    *   <li>'receivedEncodedBodyPart' - as 'receivedBodyPart', but data is still
785
+    *                                   encoded by Content-Encoding</li>
786
+    *   <li>'receivedBody'            - after receiving the complete response
787
+    *                                   body, data is HTTP_Request2_Response object</li>
788
+    * </ul>
789
+    * Different adapters may not send all the event types. Mock adapter does
790
+    * not send any events to the observers.
791
+    *
792
+    * @return   array   The array has two keys: 'name' and 'data'
793
+    */
794
+    public function getLastEvent()
795
+    {
796
+        return $this->lastEvent;
797
+    }
798
+
799
+   /**
800
+    * Sets the adapter used to actually perform the request
801
+    *
802
+    * You can pass either an instance of a class implementing HTTP_Request2_Adapter
803
+    * or a class name. The method will only try to include a file if the class
804
+    * name starts with HTTP_Request2_Adapter_, it will also try to prepend this
805
+    * prefix to the class name if it doesn't contain any underscores, so that
806
+    * <code>
807
+    * $request->setAdapter('curl');
808
+    * </code>
809
+    * will work.
810
+    *
811
+    * @param    string|HTTP_Request2_Adapter
812
+    * @return   HTTP_Request2
813
+    * @throws   HTTP_Request2_LogicException
814
+    */
815
+    public function setAdapter($adapter)
816
+    {
817
+        if (is_string($adapter)) {
818
+            if (!class_exists($adapter, false)) {
819
+                if (false === strpos($adapter, '_')) {
820
+                    $adapter = 'HTTP_Request2_Adapter_' . ucfirst($adapter);
821
+                }
822
+                if (preg_match('/^HTTP_Request2_Adapter_([a-zA-Z0-9]+)$/', $adapter)) {
823
+                    include_once str_replace('_', DIRECTORY_SEPARATOR, $adapter) . '.php';
824
+                }
825
+                if (!class_exists($adapter, false)) {
826
+                    throw new HTTP_Request2_LogicException(
827
+                        "Class {$adapter} not found",
828
+                        HTTP_Request2_Exception::MISSING_VALUE
829
+                    );
830
+                }
831
+            }
832
+            $adapter = new $adapter;
833
+        }
834
+        if (!$adapter instanceof HTTP_Request2_Adapter) {
835
+            throw new HTTP_Request2_LogicException(
836
+                'Parameter is not a HTTP request adapter',
837
+                HTTP_Request2_Exception::INVALID_ARGUMENT
838
+            );
839
+        }
840
+        $this->adapter = $adapter;
841
+
842
+        return $this;
843
+    }
844
+
845
+   /**
846
+    * Sets the cookie jar
847
+    *
848
+    * A cookie jar is used to maintain cookies across HTTP requests and
849
+    * responses. Cookies from jar will be automatically added to the request
850
+    * headers based on request URL.
851
+    *
852
+    * @param HTTP_Request2_CookieJar|bool   Existing CookieJar object, true to
853
+    *                                       create a new one, false to remove
854
+    */
855
+    public function setCookieJar($jar = true)
856
+    {
857
+        if (!class_exists('HTTP_Request2_CookieJar', false)) {
858
+            require_once 'HTTP/Request2/CookieJar.php';
859
+        }
860
+
861
+        if ($jar instanceof HTTP_Request2_CookieJar) {
862
+            $this->cookieJar = $jar;
863
+        } elseif (true === $jar) {
864
+            $this->cookieJar = new HTTP_Request2_CookieJar();
865
+        } elseif (!$jar) {
866
+            $this->cookieJar = null;
867
+        } else {
868
+            throw new HTTP_Request2_LogicException(
869
+                'Invalid parameter passed to setCookieJar()',
870
+                HTTP_Request2_Exception::INVALID_ARGUMENT
871
+            );
872
+        }
873
+
874
+        return $this;
875
+    }
876
+
877
+   /**
878
+    * Returns current CookieJar object or null if none
879
+    *
880
+    * @return HTTP_Request2_CookieJar|null
881
+    */
882
+    public function getCookieJar()
883
+    {
884
+        return $this->cookieJar;
885
+    }
886
+
887
+   /**
888
+    * Sends the request and returns the response
889
+    *
890
+    * @throws   HTTP_Request2_Exception
891
+    * @return   HTTP_Request2_Response
892
+    */
893
+    public function send()
894
+    {
895
+        // Sanity check for URL
896
+        if (!$this->url instanceof Net_URL2
897
+            || !$this->url->isAbsolute()
898
+            || !in_array(strtolower($this->url->getScheme()), array('https', 'http'))
899
+        ) {
900
+            throw new HTTP_Request2_LogicException(
901
+                'HTTP_Request2 needs an absolute HTTP(S) request URL, '
902
+                . ($this->url instanceof Net_URL2
903
+                   ? "'" . $this->url->__toString() . "'" : 'none')
904
+                . ' given',
905
+                HTTP_Request2_Exception::INVALID_ARGUMENT
906
+            );
907
+        }
908
+        if (empty($this->adapter)) {
909
+            $this->setAdapter($this->getConfig('adapter'));
910
+        }
911
+        // magic_quotes_runtime may break file uploads and chunked response
912
+        // processing; see bug #4543. Don't use ini_get() here; see bug #16440.
913
+        if ($magicQuotes = get_magic_quotes_runtime()) {
914
+            set_magic_quotes_runtime(false);
915
+        }
916
+        // force using single byte encoding if mbstring extension overloads
917
+        // strlen() and substr(); see bug #1781, bug #10605
918
+        if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) {
919
+            $oldEncoding = mb_internal_encoding();
920
+            mb_internal_encoding('iso-8859-1');
921
+        }
922
+
923
+        try {
924
+            $response = $this->adapter->sendRequest($this);
925
+        } catch (Exception $e) {
926
+        }
927
+        // cleanup in either case (poor man's "finally" clause)
928
+        if ($magicQuotes) {
929
+            set_magic_quotes_runtime(true);
930
+        }
931
+        if (!empty($oldEncoding)) {
932
+            mb_internal_encoding($oldEncoding);
933
+        }
934
+        // rethrow the exception
935
+        if (!empty($e)) {
936
+            throw $e;
937
+        }
938
+        return $response;
939
+    }
940
+
941
+   /**
942
+    * Wrapper around fopen()/fstat() used by setBody() and addUpload()
943
+    *
944
+    * @param  string|resource file name or pointer to open file
945
+    * @param  bool            whether to try autodetecting MIME type of file,
946
+    *                         will only work if $file is a filename, not pointer
947
+    * @return array array('fp' => file pointer, 'size' => file size, 'type' => MIME type)
948
+    * @throws HTTP_Request2_LogicException
949
+    */
950
+    protected function fopenWrapper($file, $detectType = false)
951
+    {
952
+        if (!is_string($file) && !is_resource($file)) {
953
+            throw new HTTP_Request2_LogicException(
954
+                "Filename or file pointer resource expected",
955
+                HTTP_Request2_Exception::INVALID_ARGUMENT
956
+            );
957
+        }
958
+        $fileData = array(
959
+            'fp'   => is_string($file)? null: $file,
960
+            'type' => 'application/octet-stream',
961
+            'size' => 0
962
+        );
963
+        if (is_string($file)) {
964
+            $track = @ini_set('track_errors', 1);
965
+            if (!($fileData['fp'] = @fopen($file, 'rb'))) {
966
+                $e = new HTTP_Request2_LogicException(
967
+                    $php_errormsg, HTTP_Request2_Exception::READ_ERROR
968
+                );
969
+            }
970
+            @ini_set('track_errors', $track);
971
+            if (isset($e)) {
972
+                throw $e;
973
+            }
974
+            if ($detectType) {
975
+                $fileData['type'] = self::detectMimeType($file);
976
+            }
977
+        }
978
+        if (!($stat = fstat($fileData['fp']))) {
979
+            throw new HTTP_Request2_LogicException(
980
+                "fstat() call failed", HTTP_Request2_Exception::READ_ERROR
981
+            );
982
+        }
983
+        $fileData['size'] = $stat['size'];
984
+
985
+        return $fileData;
986
+    }
987
+
988
+   /**
989
+    * Tries to detect MIME type of a file
990
+    *
991
+    * The method will try to use fileinfo extension if it is available,
992
+    * deprecated mime_content_type() function in the other case. If neither
993
+    * works, default 'application/octet-stream' MIME type is returned
994
+    *
995
+    * @param    string  filename
996
+    * @return   string  file MIME type
997
+    */
998
+    protected static function detectMimeType($filename)
999
+    {
1000
+        // finfo extension from PECL available
1001
+        if (function_exists('finfo_open')) {
1002
+            if (!isset(self::$_fileinfoDb)) {
1003
+                self::$_fileinfoDb = @finfo_open(FILEINFO_MIME);
1004
+            }
1005
+            if (self::$_fileinfoDb) {
1006
+                $info = finfo_file(self::$_fileinfoDb, $filename);
1007
+            }
1008
+        }
1009
+        // (deprecated) mime_content_type function available
1010
+        if (empty($info) && function_exists('mime_content_type')) {
1011
+            return mime_content_type($filename);
1012
+        }
1013
+        return empty($info)? 'application/octet-stream': $info;
1014
+    }
1015
+}
1016
+?>
1017
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2/Adapter Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2/Adapter.php Added
156
 
1
@@ -0,0 +1,154 @@
2
+<?php
3
+/**
4
+ * Base class for HTTP_Request2 adapters
5
+ *
6
+ * PHP version 5
7
+ *
8
+ * LICENSE:
9
+ *
10
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>
11
+ * All rights reserved.
12
+ *
13
+ * Redistribution and use in source and binary forms, with or without
14
+ * modification, are permitted provided that the following conditions
15
+ * are met:
16
+ *
17
+ *    * Redistributions of source code must retain the above copyright
18
+ *      notice, this list of conditions and the following disclaimer.
19
+ *    * Redistributions in binary form must reproduce the above copyright
20
+ *      notice, this list of conditions and the following disclaimer in the
21
+ *      documentation and/or other materials provided with the distribution.
22
+ *    * The names of the authors may not be used to endorse or promote products
23
+ *      derived from this software without specific prior written permission.
24
+ *
25
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ *
37
+ * @category   HTTP
38
+ * @package    HTTP_Request2
39
+ * @author     Alexey Borzov <avb@php.net>
40
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
41
+ * @version    SVN: $Id: Adapter.php 308322 2011-02-14 13:58:03Z avb $
42
+ * @link       http://pear.php.net/package/HTTP_Request2
43
+ */
44
+
45
+/**
46
+ * Class representing a HTTP response
47
+ */
48
+require_once 'HTTP/Request2/Response.php';
49
+
50
+/**
51
+ * Base class for HTTP_Request2 adapters
52
+ *
53
+ * HTTP_Request2 class itself only defines methods for aggregating the request
54
+ * data, all actual work of sending the request to the remote server and
55
+ * receiving its response is performed by adapters.
56
+ *
57
+ * @category   HTTP
58
+ * @package    HTTP_Request2
59
+ * @author     Alexey Borzov <avb@php.net>
60
+ * @version    Release: 2.0.0
61
+ */
62
+abstract class HTTP_Request2_Adapter
63
+{
64
+   /**
65
+    * A list of methods that MUST NOT have a request body, per RFC 2616
66
+    * @var  array
67
+    */
68
+    protected static $bodyDisallowed = array('TRACE');
69
+
70
+   /**
71
+    * Methods having defined semantics for request body
72
+    *
73
+    * Content-Length header (indicating that the body follows, section 4.3 of
74
+    * RFC 2616) will be sent for these methods even if no body was added
75
+    *
76
+    * @var  array
77
+    * @link http://pear.php.net/bugs/bug.php?id=12900
78
+    * @link http://pear.php.net/bugs/bug.php?id=14740
79
+    */
80
+    protected static $bodyRequired = array('POST', 'PUT');
81
+
82
+   /**
83
+    * Request being sent
84
+    * @var  HTTP_Request2
85
+    */
86
+    protected $request;
87
+
88
+   /**
89
+    * Request body
90
+    * @var  string|resource|HTTP_Request2_MultipartBody
91
+    * @see  HTTP_Request2::getBody()
92
+    */
93
+    protected $requestBody;
94
+
95
+   /**
96
+    * Length of the request body
97
+    * @var  integer
98
+    */
99
+    protected $contentLength;
100
+
101
+   /**
102
+    * Sends request to the remote server and returns its response
103
+    *
104
+    * @param    HTTP_Request2
105
+    * @return   HTTP_Request2_Response
106
+    * @throws   HTTP_Request2_Exception
107
+    */
108
+    abstract public function sendRequest(HTTP_Request2 $request);
109
+
110
+   /**
111
+    * Calculates length of the request body, adds proper headers
112
+    *
113
+    * @param    array   associative array of request headers, this method will
114
+    *                   add proper 'Content-Length' and 'Content-Type' headers
115
+    *                   to this array (or remove them if not needed)
116
+    */
117
+    protected function calculateRequestLength(&$headers)
118
+    {
119
+        $this->requestBody = $this->request->getBody();
120
+
121
+        if (is_string($this->requestBody)) {
122
+            $this->contentLength = strlen($this->requestBody);
123
+        } elseif (is_resource($this->requestBody)) {
124
+            $stat = fstat($this->requestBody);
125
+            $this->contentLength = $stat['size'];
126
+            rewind($this->requestBody);
127
+        } else {
128
+            $this->contentLength = $this->requestBody->getLength();
129
+            $headers['content-type'] = 'multipart/form-data; boundary=' .
130
+                                       $this->requestBody->getBoundary();
131
+            $this->requestBody->rewind();
132
+        }
133
+
134
+        if (in_array($this->request->getMethod(), self::$bodyDisallowed) ||
135
+            0 == $this->contentLength
136
+        ) {
137
+            // No body: send a Content-Length header nonetheless (request #12900),
138
+            // but do that only for methods that require a body (bug #14740)
139
+            if (in_array($this->request->getMethod(), self::$bodyRequired)) {
140
+                $headers['content-length'] = 0;
141
+            } else {
142
+                unset($headers['content-length']);
143
+                // if the method doesn't require a body and doesn't have a
144
+                // body, don't send a Content-Type header. (request #16799)
145
+                unset($headers['content-type']);
146
+            }
147
+        } else {
148
+            if (empty($headers['content-type'])) {
149
+                $headers['content-type'] = 'application/x-www-form-urlencoded';
150
+            }
151
+            $headers['content-length'] = $this->contentLength;
152
+        }
153
+    }
154
+}
155
+?>
156
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2/Adapter/Curl.php Added
562
 
1
@@ -0,0 +1,560 @@
2
+<?php
3
+/**
4
+ * Adapter for HTTP_Request2 wrapping around cURL extension
5
+ *
6
+ * PHP version 5
7
+ *
8
+ * LICENSE:
9
+ *
10
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>
11
+ * All rights reserved.
12
+ *
13
+ * Redistribution and use in source and binary forms, with or without
14
+ * modification, are permitted provided that the following conditions
15
+ * are met:
16
+ *
17
+ *    * Redistributions of source code must retain the above copyright
18
+ *      notice, this list of conditions and the following disclaimer.
19
+ *    * Redistributions in binary form must reproduce the above copyright
20
+ *      notice, this list of conditions and the following disclaimer in the
21
+ *      documentation and/or other materials provided with the distribution.
22
+ *    * The names of the authors may not be used to endorse or promote products
23
+ *      derived from this software without specific prior written permission.
24
+ *
25
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ *
37
+ * @category   HTTP
38
+ * @package    HTTP_Request2
39
+ * @author     Alexey Borzov <avb@php.net>
40
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
41
+ * @version    SVN: $Id: Curl.php 310800 2011-05-06 07:29:56Z avb $
42
+ * @link       http://pear.php.net/package/HTTP_Request2
43
+ */
44
+
45
+/**
46
+ * Base class for HTTP_Request2 adapters
47
+ */
48
+require_once 'HTTP/Request2/Adapter.php';
49
+
50
+/**
51
+ * Adapter for HTTP_Request2 wrapping around cURL extension
52
+ *
53
+ * @category    HTTP
54
+ * @package     HTTP_Request2
55
+ * @author      Alexey Borzov <avb@php.net>
56
+ * @version     Release: 2.0.0
57
+ */
58
+class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
59
+{
60
+   /**
61
+    * Mapping of header names to cURL options
62
+    * @var  array
63
+    */
64
+    protected static $headerMap = array(
65
+        'accept-encoding' => CURLOPT_ENCODING,
66
+        'cookie'          => CURLOPT_COOKIE,
67
+        'referer'         => CURLOPT_REFERER,
68
+        'user-agent'      => CURLOPT_USERAGENT
69
+    );
70
+
71
+   /**
72
+    * Mapping of SSL context options to cURL options
73
+    * @var  array
74
+    */
75
+    protected static $sslContextMap = array(
76
+        'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER,
77
+        'ssl_cafile'      => CURLOPT_CAINFO,
78
+        'ssl_capath'      => CURLOPT_CAPATH,
79
+        'ssl_local_cert'  => CURLOPT_SSLCERT,
80
+        'ssl_passphrase'  => CURLOPT_SSLCERTPASSWD
81
+   );
82
+
83
+   /**
84
+    * Mapping of CURLE_* constants to Exception subclasses and error codes
85
+    * @var  array
86
+    */
87
+    protected static $errorMap = array(
88
+        CURLE_UNSUPPORTED_PROTOCOL  => array('HTTP_Request2_MessageException',
89
+                                             HTTP_Request2_Exception::NON_HTTP_REDIRECT),
90
+        CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'),
91
+        CURLE_COULDNT_RESOLVE_HOST  => array('HTTP_Request2_ConnectionException'),
92
+        CURLE_COULDNT_CONNECT       => array('HTTP_Request2_ConnectionException'),
93
+        // error returned from write callback
94
+        CURLE_WRITE_ERROR           => array('HTTP_Request2_MessageException',
95
+                                             HTTP_Request2_Exception::NON_HTTP_REDIRECT),
96
+        CURLE_OPERATION_TIMEOUTED   => array('HTTP_Request2_MessageException',
97
+                                             HTTP_Request2_Exception::TIMEOUT),
98
+        CURLE_HTTP_RANGE_ERROR      => array('HTTP_Request2_MessageException'),
99
+        CURLE_SSL_CONNECT_ERROR     => array('HTTP_Request2_ConnectionException'),
100
+        CURLE_LIBRARY_NOT_FOUND     => array('HTTP_Request2_LogicException',
101
+                                             HTTP_Request2_Exception::MISCONFIGURATION),
102
+        CURLE_FUNCTION_NOT_FOUND    => array('HTTP_Request2_LogicException',
103
+                                             HTTP_Request2_Exception::MISCONFIGURATION),
104
+        CURLE_ABORTED_BY_CALLBACK   => array('HTTP_Request2_MessageException',
105
+                                             HTTP_Request2_Exception::NON_HTTP_REDIRECT),
106
+        CURLE_TOO_MANY_REDIRECTS    => array('HTTP_Request2_MessageException',
107
+                                             HTTP_Request2_Exception::TOO_MANY_REDIRECTS),
108
+        CURLE_SSL_PEER_CERTIFICATE  => array('HTTP_Request2_ConnectionException'),
109
+        CURLE_GOT_NOTHING           => array('HTTP_Request2_MessageException'),
110
+        CURLE_SSL_ENGINE_NOTFOUND   => array('HTTP_Request2_LogicException',
111
+                                             HTTP_Request2_Exception::MISCONFIGURATION),
112
+        CURLE_SSL_ENGINE_SETFAILED  => array('HTTP_Request2_LogicException',
113
+                                             HTTP_Request2_Exception::MISCONFIGURATION),
114
+        CURLE_SEND_ERROR            => array('HTTP_Request2_MessageException'),
115
+        CURLE_RECV_ERROR            => array('HTTP_Request2_MessageException'),
116
+        CURLE_SSL_CERTPROBLEM       => array('HTTP_Request2_LogicException',
117
+                                             HTTP_Request2_Exception::INVALID_ARGUMENT),
118
+        CURLE_SSL_CIPHER            => array('HTTP_Request2_ConnectionException'),
119
+        CURLE_SSL_CACERT            => array('HTTP_Request2_ConnectionException'),
120
+        CURLE_BAD_CONTENT_ENCODING  => array('HTTP_Request2_MessageException'),
121
+    );
122
+
123
+   /**
124
+    * Response being received
125
+    * @var  HTTP_Request2_Response
126
+    */
127
+    protected $response;
128
+
129
+   /**
130
+    * Whether 'sentHeaders' event was sent to observers
131
+    * @var  boolean
132
+    */
133
+    protected $eventSentHeaders = false;
134
+
135
+   /**
136
+    * Whether 'receivedHeaders' event was sent to observers
137
+    * @var boolean
138
+    */
139
+    protected $eventReceivedHeaders = false;
140
+
141
+   /**
142
+    * Position within request body
143
+    * @var  integer
144
+    * @see  callbackReadBody()
145
+    */
146
+    protected $position = 0;
147
+
148
+   /**
149
+    * Information about last transfer, as returned by curl_getinfo()
150
+    * @var  array
151
+    */
152
+    protected $lastInfo;
153
+
154
+   /**
155
+    * Creates a subclass of HTTP_Request2_Exception from curl error data
156
+    *
157
+    * @param resource curl handle
158
+    * @return HTTP_Request2_Exception
159
+    */
160
+    protected static function wrapCurlError($ch)
161
+    {
162
+        $nativeCode = curl_errno($ch);
163
+        $message    = 'Curl error: ' . curl_error($ch);
164
+        if (!isset(self::$errorMap[$nativeCode])) {
165
+            return new HTTP_Request2_Exception($message, 0, $nativeCode);
166
+        } else {
167
+            $class = self::$errorMap[$nativeCode][0];
168
+            $code  = empty(self::$errorMap[$nativeCode][1])
169
+                     ? 0 : self::$errorMap[$nativeCode][1];
170
+            return new $class($message, $code, $nativeCode);
171
+        }
172
+    }
173
+
174
+   /**
175
+    * Sends request to the remote server and returns its response
176
+    *
177
+    * @param    HTTP_Request2
178
+    * @return   HTTP_Request2_Response
179
+    * @throws   HTTP_Request2_Exception
180
+    */
181
+    public function sendRequest(HTTP_Request2 $request)
182
+    {
183
+        if (!extension_loaded('curl')) {
184
+            throw new HTTP_Request2_LogicException(
185
+                'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION
186
+            );
187
+        }
188
+
189
+        $this->request              = $request;
190
+        $this->response             = null;
191
+        $this->position             = 0;
192
+        $this->eventSentHeaders     = false;
193
+        $this->eventReceivedHeaders = false;
194
+
195
+        try {
196
+            if (false === curl_exec($ch = $this->createCurlHandle())) {
197
+                $e = self::wrapCurlError($ch);
198
+            }
199
+        } catch (Exception $e) {
200
+        }
201
+        if (isset($ch)) {
202
+            $this->lastInfo = curl_getinfo($ch);
203
+            curl_close($ch);
204
+        }
205
+
206
+        $response = $this->response;
207
+        unset($this->request, $this->requestBody, $this->response);
208
+
209
+        if (!empty($e)) {
210
+            throw $e;
211
+        }
212
+
213
+        if ($jar = $request->getCookieJar()) {
214
+            $jar->addCookiesFromResponse($response, $request->getUrl());
215
+        }
216
+
217
+        if (0 < $this->lastInfo['size_download']) {
218
+            $request->setLastEvent('receivedBody', $response);
219
+        }
220
+        return $response;
221
+    }
222
+
223
+   /**
224
+    * Returns information about last transfer
225
+    *
226
+    * @return   array   associative array as returned by curl_getinfo()
227
+    */
228
+    public function getInfo()
229
+    {
230
+        return $this->lastInfo;
231
+    }
232
+
233
+   /**
234
+    * Creates a new cURL handle and populates it with data from the request
235
+    *
236
+    * @return   resource    a cURL handle, as created by curl_init()
237
+    * @throws   HTTP_Request2_LogicException
238
+    */
239
+    protected function createCurlHandle()
240
+    {
241
+        $ch = curl_init();
242
+
243
+        curl_setopt_array($ch, array(
244
+            // setup write callbacks
245
+            CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),
246
+            CURLOPT_WRITEFUNCTION  => array($this, 'callbackWriteBody'),
247
+            // buffer size
248
+            CURLOPT_BUFFERSIZE     => $this->request->getConfig('buffer_size'),
249
+            // connection timeout
250
+            CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'),
251
+            // save full outgoing headers, in case someone is interested
252
+            CURLINFO_HEADER_OUT    => true,
253
+            // request url
254
+            CURLOPT_URL            => $this->request->getUrl()->getUrl()
255
+        ));
256
+
257
+        // set up redirects
258
+        if (!$this->request->getConfig('follow_redirects')) {
259
+            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
260
+        } else {
261
+            if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) {
262
+                throw new HTTP_Request2_LogicException(
263
+                    'Redirect support in curl is unavailable due to open_basedir or safe_mode setting',
264
+                    HTTP_Request2_Exception::MISCONFIGURATION
265
+                );
266
+            }
267
+            curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects'));
268
+            // limit redirects to http(s), works in 5.2.10+
269
+            if (defined('CURLOPT_REDIR_PROTOCOLS')) {
270
+                curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
271
+            }
272
+            // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571
273
+            if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) {
274
+                curl_setopt($ch, CURLOPT_POSTREDIR, 3);
275
+            }
276
+        }
277
+
278
+        // request timeout
279
+        if ($timeout = $this->request->getConfig('timeout')) {
280
+            curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
281
+        }
282
+
283
+        // set HTTP version
284
+        switch ($this->request->getConfig('protocol_version')) {
285
+            case '1.0':
286
+                curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
287
+                break;
288
+            case '1.1':
289
+                curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
290
+        }
291
+
292
+        // set request method
293
+        switch ($this->request->getMethod()) {
294
+            case HTTP_Request2::METHOD_GET:
295
+                curl_setopt($ch, CURLOPT_HTTPGET, true);
296
+                break;
297
+            case HTTP_Request2::METHOD_POST:
298
+                curl_setopt($ch, CURLOPT_POST, true);
299
+                break;
300
+            case HTTP_Request2::METHOD_HEAD:
301
+                curl_setopt($ch, CURLOPT_NOBODY, true);
302
+                break;
303
+            case HTTP_Request2::METHOD_PUT:
304
+                curl_setopt($ch, CURLOPT_UPLOAD, true);
305
+                break;
306
+            default:
307
+                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());
308
+        }
309
+
310
+        // set proxy, if needed
311
+        if ($host = $this->request->getConfig('proxy_host')) {
312
+            if (!($port = $this->request->getConfig('proxy_port'))) {
313
+                throw new HTTP_Request2_LogicException(
314
+                    'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE
315
+                );
316
+            }
317
+            curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);
318
+            if ($user = $this->request->getConfig('proxy_user')) {
319
+                curl_setopt($ch, CURLOPT_PROXYUSERPWD, $user . ':' .
320
+                            $this->request->getConfig('proxy_password'));
321
+                switch ($this->request->getConfig('proxy_auth_scheme')) {
322
+                    case HTTP_Request2::AUTH_BASIC:
323
+                        curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
324
+                        break;
325
+                    case HTTP_Request2::AUTH_DIGEST:
326
+                        curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);
327
+                }
328
+            }
329
+        }
330
+
331
+        // set authentication data
332
+        if ($auth = $this->request->getAuth()) {
333
+            curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']);
334
+            switch ($auth['scheme']) {
335
+                case HTTP_Request2::AUTH_BASIC:
336
+                    curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
337
+                    break;
338
+                case HTTP_Request2::AUTH_DIGEST:
339
+                    curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
340
+            }
341
+        }
342
+
343
+        // set SSL options
344
+        foreach ($this->request->getConfig() as $name => $value) {
345
+            if ('ssl_verify_host' == $name && null !== $value) {
346
+                curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);
347
+            } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {
348
+                curl_setopt($ch, self::$sslContextMap[$name], $value);
349
+            }
350
+        }
351
+
352
+        $headers = $this->request->getHeaders();
353
+        // make cURL automagically send proper header
354
+        if (!isset($headers['accept-encoding'])) {
355
+            $headers['accept-encoding'] = '';
356
+        }
357
+
358
+        if (($jar = $this->request->getCookieJar())
359
+            && ($cookies = $jar->getMatching($this->request->getUrl(), true))
360
+        ) {
361
+            $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;
362
+        }
363
+
364
+        // set headers having special cURL keys
365
+        foreach (self::$headerMap as $name => $option) {
366
+            if (isset($headers[$name])) {
367
+                curl_setopt($ch, $option, $headers[$name]);
368
+                unset($headers[$name]);
369
+            }
370
+        }
371
+
372
+        $this->calculateRequestLength($headers);
373
+        if (isset($headers['content-length'])) {
374
+            $this->workaroundPhpBug47204($ch, $headers);
375
+        }
376
+
377
+        // set headers not having special keys
378
+        $headersFmt = array();
379
+        foreach ($headers as $name => $value) {
380
+            $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
381
+            $headersFmt[]  = $canonicalName . ': ' . $value;
382
+        }
383
+        curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt);
384
+
385
+        return $ch;
386
+    }
387
+
388
+   /**
389
+    * Workaround for PHP bug #47204 that prevents rewinding request body
390
+    *
391
+    * The workaround consists of reading the entire request body into memory
392
+    * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large
393
+    * file uploads, use Socket adapter instead.
394
+    *
395
+    * @param    resource    cURL handle
396
+    * @param    array       Request headers
397
+    */
398
+    protected function workaroundPhpBug47204($ch, &$headers)
399
+    {
400
+        // no redirects, no digest auth -> probably no rewind needed
401
+        if (!$this->request->getConfig('follow_redirects')
402
+            && (!($auth = $this->request->getAuth())
403
+                || HTTP_Request2::AUTH_DIGEST != $auth['scheme'])
404
+        ) {
405
+            curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));
406
+
407
+        // rewind may be needed, read the whole body into memory
408
+        } else {
409
+            if ($this->requestBody instanceof HTTP_Request2_MultipartBody) {
410
+                $this->requestBody = $this->requestBody->__toString();
411
+
412
+            } elseif (is_resource($this->requestBody)) {
413
+                $fp = $this->requestBody;
414
+                $this->requestBody = '';
415
+                while (!feof($fp)) {
416
+                    $this->requestBody .= fread($fp, 16384);
417
+                }
418
+            }
419
+            // curl hangs up if content-length is present
420
+            unset($headers['content-length']);
421
+            curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody);
422
+        }
423
+    }
424
+
425
+   /**
426
+    * Callback function called by cURL for reading the request body
427
+    *
428
+    * @param    resource    cURL handle
429
+    * @param    resource    file descriptor (not used)
430
+    * @param    integer     maximum length of data to return
431
+    * @return   string      part of the request body, up to $length bytes
432
+    */
433
+    protected function callbackReadBody($ch, $fd, $length)
434
+    {
435
+        if (!$this->eventSentHeaders) {
436
+            $this->request->setLastEvent(
437
+                'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
438
+            );
439
+            $this->eventSentHeaders = true;
440
+        }
441
+        if (in_array($this->request->getMethod(), self::$bodyDisallowed) ||
442
+            0 == $this->contentLength || $this->position >= $this->contentLength
443
+        ) {
444
+            return '';
445
+        }
446
+        if (is_string($this->requestBody)) {
447
+            $string = substr($this->requestBody, $this->position, $length);
448
+        } elseif (is_resource($this->requestBody)) {
449
+            $string = fread($this->requestBody, $length);
450
+        } else {
451
+            $string = $this->requestBody->read($length);
452
+        }
453
+        $this->request->setLastEvent('sentBodyPart', strlen($string));
454
+        $this->position += strlen($string);
455
+        return $string;
456
+    }
457
+
458
+   /**
459
+    * Callback function called by cURL for saving the response headers
460
+    *
461
+    * @param    resource    cURL handle
462
+    * @param    string      response header (with trailing CRLF)
463
+    * @return   integer     number of bytes saved
464
+    * @see      HTTP_Request2_Response::parseHeaderLine()
465
+    */
466
+    protected function callbackWriteHeader($ch, $string)
467
+    {
468
+        // we may receive a second set of headers if doing e.g. digest auth
469
+        if ($this->eventReceivedHeaders || !$this->eventSentHeaders) {
470
+            // don't bother with 100-Continue responses (bug #15785)
471
+            if (!$this->eventSentHeaders ||
472
+                $this->response->getStatus() >= 200
473
+            ) {
474
+                $this->request->setLastEvent(
475
+                    'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
476
+                );
477
+            }
478
+            $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD);
479
+            // if body wasn't read by a callback, send event with total body size
480
+            if ($upload > $this->position) {
481
+                $this->request->setLastEvent(
482
+                    'sentBodyPart', $upload - $this->position
483
+                );
484
+                $this->position = $upload;
485
+            }
486
+            if ($upload && (!$this->eventSentHeaders
487
+                            || $this->response->getStatus() >= 200)
488
+            ) {
489
+                $this->request->setLastEvent('sentBody', $upload);
490
+            }
491
+            $this->eventSentHeaders = true;
492
+            // we'll need a new response object
493
+            if ($this->eventReceivedHeaders) {
494
+                $this->eventReceivedHeaders = false;
495
+                $this->response             = null;
496
+            }
497
+        }
498
+        if (empty($this->response)) {
499
+            $this->response = new HTTP_Request2_Response(
500
+                $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)
501
+            );
502
+        } else {
503
+            $this->response->parseHeaderLine($string);
504
+            if ('' == trim($string)) {
505
+                // don't bother with 100-Continue responses (bug #15785)
506
+                if (200 <= $this->response->getStatus()) {
507
+                    $this->request->setLastEvent('receivedHeaders', $this->response);
508
+                }
509
+
510
+                if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) {
511
+                    $redirectUrl = new Net_URL2($this->response->getHeader('location'));
512
+
513
+                    // for versions lower than 5.2.10, check the redirection URL protocol
514
+                    if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute()
515
+                        && !in_array($redirectUrl->getScheme(), array('http', 'https'))
516
+                    ) {
517
+                        return -1;
518
+                    }
519
+
520
+                    if ($jar = $this->request->getCookieJar()) {
521
+                        $jar->addCookiesFromResponse($this->response, $this->request->getUrl());
522
+                        if (!$redirectUrl->isAbsolute()) {
523
+                            $redirectUrl = $this->request->getUrl()->resolve($redirectUrl);
524
+                        }
525
+                        if ($cookies = $jar->getMatching($redirectUrl, true)) {
526
+                            curl_setopt($ch, CURLOPT_COOKIE, $cookies);
527
+                        }
528
+                    }
529
+                }
530
+                $this->eventReceivedHeaders = true;
531
+            }
532
+        }
533
+        return strlen($string);
534
+    }
535
+
536
+   /**
537
+    * Callback function called by cURL for saving the response body
538
+    *
539
+    * @param    resource    cURL handle (not used)
540
+    * @param    string      part of the response body
541
+    * @return   integer     number of bytes saved
542
+    * @see      HTTP_Request2_Response::appendBody()
543
+    */
544
+    protected function callbackWriteBody($ch, $string)
545
+    {
546
+        // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if
547
+        // response doesn't start with proper HTTP status line (see bug #15716)
548
+        if (empty($this->response)) {
549
+            throw new HTTP_Request2_MessageException(
550
+                "Malformed response: {$string}",
551
+                HTTP_Request2_Exception::MALFORMED_RESPONSE
552
+            );
553
+        }
554
+        if ($this->request->getConfig('store_body')) {
555
+            $this->response->appendBody($string);
556
+        }
557
+        $this->request->setLastEvent('receivedBodyPart', $string);
558
+        return strlen($string);
559
+    }
560
+}
561
+?>
562
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2/Adapter/Mock.php Added
174
 
1
@@ -0,0 +1,171 @@
2
+<?php
3
+/**
4
+ * Mock adapter intended for testing
5
+ *
6
+ * PHP version 5
7
+ *
8
+ * LICENSE:
9
+ *
10
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>
11
+ * All rights reserved.
12
+ *
13
+ * Redistribution and use in source and binary forms, with or without
14
+ * modification, are permitted provided that the following conditions
15
+ * are met:
16
+ *
17
+ *    * Redistributions of source code must retain the above copyright
18
+ *      notice, this list of conditions and the following disclaimer.
19
+ *    * Redistributions in binary form must reproduce the above copyright
20
+ *      notice, this list of conditions and the following disclaimer in the
21
+ *      documentation and/or other materials provided with the distribution.
22
+ *    * The names of the authors may not be used to endorse or promote products
23
+ *      derived from this software without specific prior written permission.
24
+ *
25
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ *
37
+ * @category   HTTP
38
+ * @package    HTTP_Request2
39
+ * @author     Alexey Borzov <avb@php.net>
40
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
41
+ * @version    SVN: $Id: Mock.php 308322 2011-02-14 13:58:03Z avb $
42
+ * @link       http://pear.php.net/package/HTTP_Request2
43
+ */
44
+
45
+/**
46
+ * Base class for HTTP_Request2 adapters
47
+ */
48
+require_once 'HTTP/Request2/Adapter.php';
49
+
50
+/**
51
+ * Mock adapter intended for testing
52
+ *
53
+ * Can be used to test applications depending on HTTP_Request2 package without
54
+ * actually performing any HTTP requests. This adapter will return responses
55
+ * previously added via addResponse()
56
+ * <code>
57
+ * $mock = new HTTP_Request2_Adapter_Mock();
58
+ * $mock->addResponse("HTTP/1.1 ... ");
59
+ *
60
+ * $request = new HTTP_Request2();
61
+ * $request->setAdapter($mock);
62
+ *
63
+ * // This will return the response set above
64
+ * $response = $req->send();
65
+ * </code>
66
+ *
67
+ * @category   HTTP
68
+ * @package    HTTP_Request2
69
+ * @author     Alexey Borzov <avb@php.net>
70
+ * @version    Release: 2.0.0
71
+ */
72
+class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter
73
+{
74
+   /**
75
+    * A queue of responses to be returned by sendRequest()
76
+    * @var  array
77
+    */
78
+    protected $responses = array();
79
+
80
+   /**
81
+    * Returns the next response from the queue built by addResponse()
82
+    *
83
+    * If the queue is empty it will return default empty response with status 400,
84
+    * if an Exception object was added to the queue it will be thrown.
85
+    *
86
+    * @param    HTTP_Request2
87
+    * @return   HTTP_Request2_Response
88
+    * @throws   Exception
89
+    */
90
+    public function sendRequest(HTTP_Request2 $request)
91
+    {
92
+        if (count($this->responses) > 0) {
93
+            $response = array_shift($this->responses);
94
+            if ($response instanceof HTTP_Request2_Response) {
95
+                return $response;
96
+            } else {
97
+                // rethrow the exception
98
+                $class   = get_class($response);
99
+                $message = $response->getMessage();
100
+                $code    = $response->getCode();
101
+                throw new $class($message, $code);
102
+            }
103
+        } else {
104
+            return self::createResponseFromString("HTTP/1.1 400 Bad Request\r\n\r\n");
105
+        }
106
+    }
107
+
108
+   /**
109
+    * Adds response to the queue
110
+    *
111
+    * @param    mixed   either a string, a pointer to an open file,
112
+    *                   an instance of HTTP_Request2_Response or Exception
113
+    * @throws   HTTP_Request2_Exception
114
+    */
115
+    public function addResponse($response)
116
+    {
117
+        if (is_string($response)) {
118
+            $response = self::createResponseFromString($response);
119
+        } elseif (is_resource($response)) {
120
+            $response = self::createResponseFromFile($response);
121
+        } elseif (!$response instanceof HTTP_Request2_Response &&
122
+                  !$response instanceof Exception
123
+        ) {
124
+            throw new HTTP_Request2_Exception('Parameter is not a valid response');
125
+        }
126
+        $this->responses[] = $response;
127
+    }
128
+
129
+   /**
130
+    * Creates a new HTTP_Request2_Response object from a string
131
+    *
132
+    * @param    string
133
+    * @return   HTTP_Request2_Response
134
+    * @throws   HTTP_Request2_Exception
135
+    */
136
+    public static function createResponseFromString($str)
137
+    {
138
+        $parts       = preg_split('!(\r?\n){2}!m', $str, 2);
139
+        $headerLines = explode("\n", $parts[0]);
140
+        $response    = new HTTP_Request2_Response(array_shift($headerLines));
141
+        foreach ($headerLines as $headerLine) {
142
+            $response->parseHeaderLine($headerLine);
143
+        }
144
+        $response->parseHeaderLine('');
145
+        if (isset($parts[1])) {
146
+            $response->appendBody($parts[1]);
147
+        }
148
+        return $response;
149
+    }
150
+
151
+   /**
152
+    * Creates a new HTTP_Request2_Response object from a file
153
+    *
154
+    * @param    resource    file pointer returned by fopen()
155
+    * @return   HTTP_Request2_Response
156
+    * @throws   HTTP_Request2_Exception
157
+    */
158
+    public static function createResponseFromFile($fp)
159
+    {
160
+        $response = new HTTP_Request2_Response(fgets($fp));
161
+        do {
162
+            $headerLine = fgets($fp);
163
+            $response->parseHeaderLine($headerLine);
164
+        } while ('' != trim($headerLine));
165
+
166
+        while (!feof($fp)) {
167
+            $response->appendBody(fread($fp, 8192));
168
+        }
169
+        return $response;
170
+    }
171
+}
172
+?>
173
\ No newline at end of file
174
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2/Adapter/Socket.php Added
1087
 
1
@@ -0,0 +1,1084 @@
2
+<?php
3
+/**
4
+ * Socket-based adapter for HTTP_Request2
5
+ *
6
+ * PHP version 5
7
+ *
8
+ * LICENSE:
9
+ *
10
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>
11
+ * All rights reserved.
12
+ *
13
+ * Redistribution and use in source and binary forms, with or without
14
+ * modification, are permitted provided that the following conditions
15
+ * are met:
16
+ *
17
+ *    * Redistributions of source code must retain the above copyright
18
+ *      notice, this list of conditions and the following disclaimer.
19
+ *    * Redistributions in binary form must reproduce the above copyright
20
+ *      notice, this list of conditions and the following disclaimer in the
21
+ *      documentation and/or other materials provided with the distribution.
22
+ *    * The names of the authors may not be used to endorse or promote products
23
+ *      derived from this software without specific prior written permission.
24
+ *
25
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ *
37
+ * @category   HTTP
38
+ * @package    HTTP_Request2
39
+ * @author     Alexey Borzov <avb@php.net>
40
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
41
+ * @version    SVN: $Id: Socket.php 309921 2011-04-03 16:43:02Z avb $
42
+ * @link       http://pear.php.net/package/HTTP_Request2
43
+ */
44
+
45
+/**
46
+ * Base class for HTTP_Request2 adapters
47
+ */
48
+require_once 'HTTP/Request2/Adapter.php';
49
+
50
+/**
51
+ * Socket-based adapter for HTTP_Request2
52
+ *
53
+ * This adapter uses only PHP sockets and will work on almost any PHP
54
+ * environment. Code is based on original HTTP_Request PEAR package.
55
+ *
56
+ * @category    HTTP
57
+ * @package     HTTP_Request2
58
+ * @author      Alexey Borzov <avb@php.net>
59
+ * @version     Release: 2.0.0
60
+ */
61
+class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
62
+{
63
+   /**
64
+    * Regular expression for 'token' rule from RFC 2616
65
+    */
66
+    const REGEXP_TOKEN = '[^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+';
67
+
68
+   /**
69
+    * Regular expression for 'quoted-string' rule from RFC 2616
70
+    */
71
+    const REGEXP_QUOTED_STRING = '"(?:\\\\.|[^\\\\"])*"';
72
+
73
+   /**
74
+    * Connected sockets, needed for Keep-Alive support
75
+    * @var  array
76
+    * @see  connect()
77
+    */
78
+    protected static $sockets = array();
79
+
80
+   /**
81
+    * Data for digest authentication scheme
82
+    *
83
+    * The keys for the array are URL prefixes.
84
+    *
85
+    * The values are associative arrays with data (realm, nonce, nonce-count,
86
+    * opaque...) needed for digest authentication. Stored here to prevent making
87
+    * duplicate requests to digest-protected resources after we have already
88
+    * received the challenge.
89
+    *
90
+    * @var  array
91
+    */
92
+    protected static $challenges = array();
93
+
94
+   /**
95
+    * Connected socket
96
+    * @var  resource
97
+    * @see  connect()
98
+    */
99
+    protected $socket;
100
+
101
+   /**
102
+    * Challenge used for server digest authentication
103
+    * @var  array
104
+    */
105
+    protected $serverChallenge;
106
+
107
+   /**
108
+    * Challenge used for proxy digest authentication
109
+    * @var  array
110
+    */
111
+    protected $proxyChallenge;
112
+
113
+   /**
114
+    * Sum of start time and global timeout, exception will be thrown if request continues past this time
115
+    * @var  integer
116
+    */
117
+    protected $deadline = null;
118
+
119
+   /**
120
+    * Remaining length of the current chunk, when reading chunked response
121
+    * @var  integer
122
+    * @see  readChunked()
123
+    */
124
+    protected $chunkLength = 0;
125
+
126
+   /**
127
+    * Remaining amount of redirections to follow
128
+    *
129
+    * Starts at 'max_redirects' configuration parameter and is reduced on each
130
+    * subsequent redirect. An Exception will be thrown once it reaches zero.
131
+    *
132
+    * @var  integer
133
+    */
134
+    protected $redirectCountdown = null;
135
+
136
+   /**
137
+    * Sends request to the remote server and returns its response
138
+    *
139
+    * @param    HTTP_Request2
140
+    * @return   HTTP_Request2_Response
141
+    * @throws   HTTP_Request2_Exception
142
+    */
143
+    public function sendRequest(HTTP_Request2 $request)
144
+    {
145
+        $this->request = $request;
146
+
147
+        // Use global request timeout if given, see feature requests #5735, #8964
148
+        if ($timeout = $request->getConfig('timeout')) {
149
+            $this->deadline = time() + $timeout;
150
+        } else {
151
+            $this->deadline = null;
152
+        }
153
+
154
+        try {
155
+            $keepAlive = $this->connect();
156
+            $headers   = $this->prepareHeaders();
157
+            if (false === @fwrite($this->socket, $headers, strlen($headers))) {
158
+                throw new HTTP_Request2_MessageException('Error writing request');
159
+            }
160
+            // provide request headers to the observer, see request #7633
161
+            $this->request->setLastEvent('sentHeaders', $headers);
162
+            $this->writeBody();
163
+
164
+            if ($this->deadline && time() > $this->deadline) {
165
+                throw new HTTP_Request2_MessageException(
166
+                    'Request timed out after ' .
167
+                    $request->getConfig('timeout') . ' second(s)',
168
+                    HTTP_Request2_Exception::TIMEOUT
169
+                );
170
+            }
171
+
172
+            $response = $this->readResponse();
173
+
174
+            if ($jar = $request->getCookieJar()) {
175
+                $jar->addCookiesFromResponse($response, $request->getUrl());
176
+            }
177
+
178
+            if (!$this->canKeepAlive($keepAlive, $response)) {
179
+                $this->disconnect();
180
+            }
181
+
182
+            if ($this->shouldUseProxyDigestAuth($response)) {
183
+                return $this->sendRequest($request);
184
+            }
185
+            if ($this->shouldUseServerDigestAuth($response)) {
186
+                return $this->sendRequest($request);
187
+            }
188
+            if ($authInfo = $response->getHeader('authentication-info')) {
189
+                $this->updateChallenge($this->serverChallenge, $authInfo);
190
+            }
191
+            if ($proxyInfo = $response->getHeader('proxy-authentication-info')) {
192
+                $this->updateChallenge($this->proxyChallenge, $proxyInfo);
193
+            }
194
+
195
+        } catch (Exception $e) {
196
+            $this->disconnect();
197
+        }
198
+
199
+        unset($this->request, $this->requestBody);
200
+
201
+        if (!empty($e)) {
202
+            $this->redirectCountdown = null;
203
+            throw $e;
204
+        }
205
+
206
+        if (!$request->getConfig('follow_redirects') || !$response->isRedirect()) {
207
+            $this->redirectCountdown = null;
208
+            return $response;
209
+        } else {
210
+            return $this->handleRedirect($request, $response);
211
+        }
212
+    }
213
+
214
+   /**
215
+    * Connects to the remote server
216
+    *
217
+    * @return   bool    whether the connection can be persistent
218
+    * @throws   HTTP_Request2_Exception
219
+    */
220
+    protected function connect()
221
+    {
222
+        $secure  = 0 == strcasecmp($this->request->getUrl()->getScheme(), 'https');
223
+        $tunnel  = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod();
224
+        $headers = $this->request->getHeaders();
225
+        $reqHost = $this->request->getUrl()->getHost();
226
+        if (!($reqPort = $this->request->getUrl()->getPort())) {
227
+            $reqPort = $secure? 443: 80;
228
+        }
229
+
230
+        if ($host = $this->request->getConfig('proxy_host')) {
231
+            if (!($port = $this->request->getConfig('proxy_port'))) {
232
+                throw new HTTP_Request2_LogicException(
233
+                    'Proxy port not provided',
234
+                    HTTP_Request2_Exception::MISSING_VALUE
235
+                );
236
+            }
237
+            $proxy = true;
238
+        } else {
239
+            $host  = $reqHost;
240
+            $port  = $reqPort;
241
+            $proxy = false;
242
+        }
243
+
244
+        if ($tunnel && !$proxy) {
245
+            throw new HTTP_Request2_LogicException(
246
+                "Trying to perform CONNECT request without proxy",
247
+                HTTP_Request2_Exception::MISSING_VALUE
248
+            );
249
+        }
250
+        if ($secure && !in_array('ssl', stream_get_transports())) {
251
+            throw new HTTP_Request2_LogicException(
252
+                'Need OpenSSL support for https:// requests',
253
+                HTTP_Request2_Exception::MISCONFIGURATION
254
+            );
255
+        }
256
+
257
+        // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
258
+        // connection token to a proxy server...
259
+        if ($proxy && !$secure &&
260
+            !empty($headers['connection']) && 'Keep-Alive' == $headers['connection']
261
+        ) {
262
+            $this->request->setHeader('connection');
263
+        }
264
+
265
+        $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') &&
266
+                      empty($headers['connection'])) ||
267
+                     (!empty($headers['connection']) &&
268
+                      'Keep-Alive' == $headers['connection']);
269
+        $host = ((!$secure || $proxy)? 'tcp://': 'ssl://') . $host;
270
+
271
+        $options = array();
272
+        if ($secure || $tunnel) {
273
+            foreach ($this->request->getConfig() as $name => $value) {
274
+                if ('ssl_' == substr($name, 0, 4) && null !== $value) {
275
+                    if ('ssl_verify_host' == $name) {
276
+                        if ($value) {
277
+                            $options['CN_match'] = $reqHost;
278
+                        }
279
+                    } else {
280
+                        $options[substr($name, 4)] = $value;
281
+                    }
282
+                }
283
+            }
284
+            ksort($options);
285
+        }
286
+
287
+        // Changing SSL context options after connection is established does *not*
288
+        // work, we need a new connection if options change
289
+        $remote    = $host . ':' . $port;
290
+        $socketKey = $remote . (($secure && $proxy)? "->{$reqHost}:{$reqPort}": '') .
291
+                     (empty($options)? '': ':' . serialize($options));
292
+        unset($this->socket);
293
+
294
+        // We use persistent connections and have a connected socket?
295
+        // Ensure that the socket is still connected, see bug #16149
296
+        if ($keepAlive && !empty(self::$sockets[$socketKey]) &&
297
+            !feof(self::$sockets[$socketKey])
298
+        ) {
299
+            $this->socket =& self::$sockets[$socketKey];
300
+
301
+        } elseif ($secure && $proxy && !$tunnel) {
302
+            $this->establishTunnel();
303
+            $this->request->setLastEvent(
304
+                'connect', "ssl://{$reqHost}:{$reqPort} via {$host}:{$port}"
305
+            );
306
+            self::$sockets[$socketKey] =& $this->socket;
307
+
308
+        } else {
309
+            // Set SSL context options if doing HTTPS request or creating a tunnel
310
+            $context = stream_context_create();
311
+            foreach ($options as $name => $value) {
312
+                if (!stream_context_set_option($context, 'ssl', $name, $value)) {
313
+                    throw new HTTP_Request2_LogicException(
314
+                        "Error setting SSL context option '{$name}'"
315
+                    );
316
+                }
317
+            }
318
+            $track = @ini_set('track_errors', 1);
319
+            $this->socket = @stream_socket_client(
320
+                $remote, $errno, $errstr,
321
+                $this->request->getConfig('connect_timeout'),
322
+                STREAM_CLIENT_CONNECT, $context
323
+            );
324
+            if (!$this->socket) {
325
+                $e = new HTTP_Request2_ConnectionException(
326
+                    "Unable to connect to {$remote}. Error: "
327
+                     . (empty($errstr)? $php_errormsg: $errstr), 0, $errno
328
+                );
329
+            }
330
+            @ini_set('track_errors', $track);
331
+            if (isset($e)) {
332
+                throw $e;
333
+            }
334
+            $this->request->setLastEvent('connect', $remote);
335
+            self::$sockets[$socketKey] =& $this->socket;
336
+        }
337
+        return $keepAlive;
338
+    }
339
+
340
+   /**
341
+    * Establishes a tunnel to a secure remote server via HTTP CONNECT request
342
+    *
343
+    * This method will fail if 'ssl_verify_peer' is enabled. Probably because PHP
344
+    * sees that we are connected to a proxy server (duh!) rather than the server
345
+    * that presents its certificate.
346
+    *
347
+    * @link     http://tools.ietf.org/html/rfc2817#section-5.2
348
+    * @throws   HTTP_Request2_Exception
349
+    */
350
+    protected function establishTunnel()
351
+    {
352
+        $donor   = new self;
353
+        $connect = new HTTP_Request2(
354
+            $this->request->getUrl(), HTTP_Request2::METHOD_CONNECT,
355
+            array_merge($this->request->getConfig(),
356
+                        array('adapter' => $donor))
357
+        );
358
+        $response = $connect->send();
359
+        // Need any successful (2XX) response
360
+        if (200 > $response->getStatus() || 300 <= $response->getStatus()) {
361
+            throw new HTTP_Request2_ConnectionException(
362
+                'Failed to connect via HTTPS proxy. Proxy response: ' .
363
+                $response->getStatus() . ' ' . $response->getReasonPhrase()
364
+            );
365
+        }
366
+        $this->socket = $donor->socket;
367
+
368
+        $modes = array(
369
+            STREAM_CRYPTO_METHOD_TLS_CLIENT,
370
+            STREAM_CRYPTO_METHOD_SSLv3_CLIENT,
371
+            STREAM_CRYPTO_METHOD_SSLv23_CLIENT,
372
+            STREAM_CRYPTO_METHOD_SSLv2_CLIENT
373
+        );
374
+
375
+        foreach ($modes as $mode) {
376
+            if (stream_socket_enable_crypto($this->socket, true, $mode)) {
377
+                return;
378
+            }
379
+        }
380
+        throw new HTTP_Request2_ConnectionException(
381
+            'Failed to enable secure connection when connecting through proxy'
382
+        );
383
+    }
384
+
385
+   /**
386
+    * Checks whether current connection may be reused or should be closed
387
+    *
388
+    * @param    boolean                 whether connection could be persistent
389
+    *                                   in the first place
390
+    * @param    HTTP_Request2_Response  response object to check
391
+    * @return   boolean
392
+    */
393
+    protected function canKeepAlive($requestKeepAlive, HTTP_Request2_Response $response)
394
+    {
395
+        // Do not close socket on successful CONNECT request
396
+        if (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod() &&
397
+            200 <= $response->getStatus() && 300 > $response->getStatus()
398
+        ) {
399
+            return true;
400
+        }
401
+
402
+        $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding'))
403
+                       || null !== $response->getHeader('content-length')
404
+                       // no body possible for such responses, see also request #17031
405
+                       || HTTP_Request2::METHOD_HEAD == $this->request->getMethod()
406
+                       || in_array($response->getStatus(), array(204, 304));
407
+        $persistent  = 'keep-alive' == strtolower($response->getHeader('connection')) ||
408
+                       (null === $response->getHeader('connection') &&
409
+                        '1.1' == $response->getVersion());
410
+        return $requestKeepAlive && $lengthKnown && $persistent;
411
+    }
412
+
413
+   /**
414
+    * Disconnects from the remote server
415
+    */
416
+    protected function disconnect()
417
+    {
418
+        if (is_resource($this->socket)) {
419
+            fclose($this->socket);
420
+            $this->socket = null;
421
+            $this->request->setLastEvent('disconnect');
422
+        }
423
+    }
424
+
425
+   /**
426
+    * Handles HTTP redirection
427
+    *
428
+    * This method will throw an Exception if redirect to a non-HTTP(S) location
429
+    * is attempted, also if number of redirects performed already is equal to
430
+    * 'max_redirects' configuration parameter.
431
+    *
432
+    * @param    HTTP_Request2               Original request
433
+    * @param    HTTP_Request2_Response      Response containing redirect
434
+    * @return   HTTP_Request2_Response      Response from a new location
435
+    * @throws   HTTP_Request2_Exception
436
+    */
437
+    protected function handleRedirect(HTTP_Request2 $request,
438
+                                      HTTP_Request2_Response $response)
439
+    {
440
+        if (is_null($this->redirectCountdown)) {
441
+            $this->redirectCountdown = $request->getConfig('max_redirects');
442
+        }
443
+        if (0 == $this->redirectCountdown) {
444
+            $this->redirectCountdown = null;
445
+            // Copying cURL behaviour
446
+            throw new HTTP_Request2_MessageException (
447
+                'Maximum (' . $request->getConfig('max_redirects') . ') redirects followed',
448
+                HTTP_Request2_Exception::TOO_MANY_REDIRECTS
449
+            );
450
+        }
451
+        $redirectUrl = new Net_URL2(
452
+            $response->getHeader('location'),
453
+            array(Net_URL2::OPTION_USE_BRACKETS => $request->getConfig('use_brackets'))
454
+        );
455
+        // refuse non-HTTP redirect
456
+        if ($redirectUrl->isAbsolute()
457
+            && !in_array($redirectUrl->getScheme(), array('http', 'https'))
458
+        ) {
459
+            $this->redirectCountdown = null;
460
+            throw new HTTP_Request2_MessageException(
461
+                'Refusing to redirect to a non-HTTP URL ' . $redirectUrl->__toString(),
462
+                HTTP_Request2_Exception::NON_HTTP_REDIRECT
463
+            );
464
+        }
465
+        // Theoretically URL should be absolute (see http://tools.ietf.org/html/rfc2616#section-14.30),
466
+        // but in practice it is often not
467
+        if (!$redirectUrl->isAbsolute()) {
468
+            $redirectUrl = $request->getUrl()->resolve($redirectUrl);
469
+        }
470
+        $redirect = clone $request;
471
+        $redirect->setUrl($redirectUrl);
472
+        if (303 == $response->getStatus() || (!$request->getConfig('strict_redirects')
473
+             && in_array($response->getStatus(), array(301, 302)))
474
+        ) {
475
+            $redirect->setMethod(HTTP_Request2::METHOD_GET);
476
+            $redirect->setBody('');
477
+        }
478
+
479
+        if (0 < $this->redirectCountdown) {
480
+            $this->redirectCountdown--;
481
+        }
482
+        return $this->sendRequest($redirect);
483
+    }
484
+
485
+   /**
486
+    * Checks whether another request should be performed with server digest auth
487
+    *
488
+    * Several conditions should be satisfied for it to return true:
489
+    *   - response status should be 401
490
+    *   - auth credentials should be set in the request object
491
+    *   - response should contain WWW-Authenticate header with digest challenge
492
+    *   - there is either no challenge stored for this URL or new challenge
493
+    *     contains stale=true parameter (in other case we probably just failed
494
+    *     due to invalid username / password)
495
+    *
496
+    * The method stores challenge values in $challenges static property
497
+    *
498
+    * @param    HTTP_Request2_Response  response to check
499
+    * @return   boolean whether another request should be performed
500
+    * @throws   HTTP_Request2_Exception in case of unsupported challenge parameters
501
+    */
502
+    protected function shouldUseServerDigestAuth(HTTP_Request2_Response $response)
503
+    {
504
+        // no sense repeating a request if we don't have credentials
505
+        if (401 != $response->getStatus() || !$this->request->getAuth()) {
506
+            return false;
507
+        }
508
+        if (!$challenge = $this->parseDigestChallenge($response->getHeader('www-authenticate'))) {
509
+            return false;
510
+        }
511
+
512
+        $url    = $this->request->getUrl();
513
+        $scheme = $url->getScheme();
514
+        $host   = $scheme . '://' . $url->getHost();
515
+        if ($port = $url->getPort()) {
516
+            if ((0 == strcasecmp($scheme, 'http') && 80 != $port) ||
517
+                (0 == strcasecmp($scheme, 'https') && 443 != $port)
518
+            ) {
519
+                $host .= ':' . $port;
520
+            }
521
+        }
522
+
523
+        if (!empty($challenge['domain'])) {
524
+            $prefixes = array();
525
+            foreach (preg_split('/\\s+/', $challenge['domain']) as $prefix) {
526
+                // don't bother with different servers
527
+                if ('/' == substr($prefix, 0, 1)) {
528
+                    $prefixes[] = $host . $prefix;
529
+                }
530
+            }
531
+        }
532
+        if (empty($prefixes)) {
533
+            $prefixes = array($host . '/');
534
+        }
535
+
536
+        $ret = true;
537
+        foreach ($prefixes as $prefix) {
538
+            if (!empty(self::$challenges[$prefix]) &&
539
+                (empty($challenge['stale']) || strcasecmp('true', $challenge['stale']))
540
+            ) {
541
+                // probably credentials are invalid
542
+                $ret = false;
543
+            }
544
+            self::$challenges[$prefix] =& $challenge;
545
+        }
546
+        return $ret;
547
+    }
548
+
549
+   /**
550
+    * Checks whether another request should be performed with proxy digest auth
551
+    *
552
+    * Several conditions should be satisfied for it to return true:
553
+    *   - response status should be 407
554
+    *   - proxy auth credentials should be set in the request object
555
+    *   - response should contain Proxy-Authenticate header with digest challenge
556
+    *   - there is either no challenge stored for this proxy or new challenge
557
+    *     contains stale=true parameter (in other case we probably just failed
558
+    *     due to invalid username / password)
559
+    *
560
+    * The method stores challenge values in $challenges static property
561
+    *
562
+    * @param    HTTP_Request2_Response  response to check
563
+    * @return   boolean whether another request should be performed
564
+    * @throws   HTTP_Request2_Exception in case of unsupported challenge parameters
565
+    */
566
+    protected function shouldUseProxyDigestAuth(HTTP_Request2_Response $response)
567
+    {
568
+        if (407 != $response->getStatus() || !$this->request->getConfig('proxy_user')) {
569
+            return false;
570
+        }
571
+        if (!($challenge = $this->parseDigestChallenge($response->getHeader('proxy-authenticate')))) {
572
+            return false;
573
+        }
574
+
575
+        $key = 'proxy://' . $this->request->getConfig('proxy_host') .
576
+               ':' . $this->request->getConfig('proxy_port');
577
+
578
+        if (!empty(self::$challenges[$key]) &&
579
+            (empty($challenge['stale']) || strcasecmp('true', $challenge['stale']))
580
+        ) {
581
+            $ret = false;
582
+        } else {
583
+            $ret = true;
584
+        }
585
+        self::$challenges[$key] = $challenge;
586
+        return $ret;
587
+    }
588
+
589
+   /**
590
+    * Extracts digest method challenge from (WWW|Proxy)-Authenticate header value
591
+    *
592
+    * There is a problem with implementation of RFC 2617: several of the parameters
593
+    * are defined as quoted-string there and thus may contain backslash escaped
594
+    * double quotes (RFC 2616, section 2.2). However, RFC 2617 defines unq(X) as
595
+    * just value of quoted-string X without surrounding quotes, it doesn't speak
596
+    * about removing backslash escaping.
597
+    *
598
+    * Now realm parameter is user-defined and human-readable, strange things
599
+    * happen when it contains quotes:
600
+    *   - Apache allows quotes in realm, but apparently uses realm value without
601
+    *     backslashes for digest computation
602
+    *   - Squid allows (manually escaped) quotes there, but it is impossible to
603
+    *     authorize with either escaped or unescaped quotes used in digest,
604
+    *     probably it can't parse the response (?)
605
+    *   - Both IE and Firefox display realm value with backslashes in
606
+    *     the password popup and apparently use the same value for digest
607
+    *
608
+    * HTTP_Request2 follows IE and Firefox (and hopefully RFC 2617) in
609
+    * quoted-string handling, unfortunately that means failure to authorize
610
+    * sometimes
611
+    *
612
+    * @param    string  value of WWW-Authenticate or Proxy-Authenticate header
613
+    * @return   mixed   associative array with challenge parameters, false if
614
+    *                   no challenge is present in header value
615
+    * @throws   HTTP_Request2_NotImplementedException in case of unsupported challenge parameters
616
+    */
617
+    protected function parseDigestChallenge($headerValue)
618
+    {
619
+        $authParam   = '(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' .
620
+                       self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')';
621
+        $challenge   = "!(?<=^|\\s|,)Digest ({$authParam}\\s*(,\\s*|$))+!";
622
+        if (!preg_match($challenge, $headerValue, $matches)) {
623
+            return false;
624
+        }
625
+
626
+        preg_match_all('!' . $authParam . '!', $matches[0], $params);
627
+        $paramsAry   = array();
628
+        $knownParams = array('realm', 'domain', 'nonce', 'opaque', 'stale',
629
+                             'algorithm', 'qop');
630
+        for ($i = 0; $i < count($params[0]); $i++) {
631
+            // section 3.2.1: Any unrecognized directive MUST be ignored.
632
+            if (in_array($params[1][$i], $knownParams)) {
633
+                if ('"' == substr($params[2][$i], 0, 1)) {
634
+                    $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1);
635
+                } else {
636
+                    $paramsAry[$params[1][$i]] = $params[2][$i];
637
+                }
638
+            }
639
+        }
640
+        // we only support qop=auth
641
+        if (!empty($paramsAry['qop']) &&
642
+            !in_array('auth', array_map('trim', explode(',', $paramsAry['qop'])))
643
+        ) {
644
+            throw new HTTP_Request2_NotImplementedException(
645
+                "Only 'auth' qop is currently supported in digest authentication, " .
646
+                "server requested '{$paramsAry['qop']}'"
647
+            );
648
+        }
649
+        // we only support algorithm=MD5
650
+        if (!empty($paramsAry['algorithm']) && 'MD5' != $paramsAry['algorithm']) {
651
+            throw new HTTP_Request2_NotImplementedException(
652
+                "Only 'MD5' algorithm is currently supported in digest authentication, " .
653
+                "server requested '{$paramsAry['algorithm']}'"
654
+            );
655
+        }
656
+
657
+        return $paramsAry;
658
+    }
659
+
660
+   /**
661
+    * Parses [Proxy-]Authentication-Info header value and updates challenge
662
+    *
663
+    * @param    array   challenge to update
664
+    * @param    string  value of [Proxy-]Authentication-Info header
665
+    * @todo     validate server rspauth response
666
+    */
667
+    protected function updateChallenge(&$challenge, $headerValue)
668
+    {
669
+        $authParam   = '!(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' .
670
+                       self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')!';
671
+        $paramsAry   = array();
672
+
673
+        preg_match_all($authParam, $headerValue, $params);
674
+        for ($i = 0; $i < count($params[0]); $i++) {
675
+            if ('"' == substr($params[2][$i], 0, 1)) {
676
+                $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1);
677
+            } else {
678
+                $paramsAry[$params[1][$i]] = $params[2][$i];
679
+            }
680
+        }
681
+        // for now, just update the nonce value
682
+        if (!empty($paramsAry['nextnonce'])) {
683
+            $challenge['nonce'] = $paramsAry['nextnonce'];
684
+            $challenge['nc']    = 1;
685
+        }
686
+    }
687
+
688
+   /**
689
+    * Creates a value for [Proxy-]Authorization header when using digest authentication
690
+    *
691
+    * @param    string  user name
692
+    * @param    string  password
693
+    * @param    string  request URL
694
+    * @param    array   digest challenge parameters
695
+    * @return   string  value of [Proxy-]Authorization request header
696
+    * @link     http://tools.ietf.org/html/rfc2617#section-3.2.2
697
+    */
698
+    protected function createDigestResponse($user, $password, $url, &$challenge)
699
+    {
700
+        if (false !== ($q = strpos($url, '?')) &&
701
+            $this->request->getConfig('digest_compat_ie')
702
+        ) {
703
+            $url = substr($url, 0, $q);
704
+        }
705
+
706
+        $a1 = md5($user . ':' . $challenge['realm'] . ':' . $password);
707
+        $a2 = md5($this->request->getMethod() . ':' . $url);
708
+
709
+        if (empty($challenge['qop'])) {
710
+            $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $a2);
711
+        } else {
712
+            $challenge['cnonce'] = 'Req2.' . rand();
713
+            if (empty($challenge['nc'])) {
714
+                $challenge['nc'] = 1;
715
+            }
716
+            $nc     = sprintf('%08x', $challenge['nc']++);
717
+            $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $nc . ':' .
718
+                          $challenge['cnonce'] . ':auth:' . $a2);
719
+        }
720
+        return 'Digest username="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $user) . '", ' .
721
+               'realm="' . $challenge['realm'] . '", ' .
722
+               'nonce="' . $challenge['nonce'] . '", ' .
723
+               'uri="' . $url . '", ' .
724
+               'response="' . $digest . '"' .
725
+               (!empty($challenge['opaque'])?
726
+                ', opaque="' . $challenge['opaque'] . '"':
727
+                '') .
728
+               (!empty($challenge['qop'])?
729
+                ', qop="auth", nc=' . $nc . ', cnonce="' . $challenge['cnonce'] . '"':
730
+                '');
731
+    }
732
+
733
+   /**
734
+    * Adds 'Authorization' header (if needed) to request headers array
735
+    *
736
+    * @param    array   request headers
737
+    * @param    string  request host (needed for digest authentication)
738
+    * @param    string  request URL (needed for digest authentication)
739
+    * @throws   HTTP_Request2_NotImplementedException
740
+    */
741
+    protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl)
742
+    {
743
+        if (!($auth = $this->request->getAuth())) {
744
+            return;
745
+        }
746
+        switch ($auth['scheme']) {
747
+            case HTTP_Request2::AUTH_BASIC:
748
+                $headers['authorization'] =
749
+                    'Basic ' . base64_encode($auth['user'] . ':' . $auth['password']);
750
+                break;
751
+
752
+            case HTTP_Request2::AUTH_DIGEST:
753
+                unset($this->serverChallenge);
754
+                $fullUrl = ('/' == $requestUrl[0])?
755
+                           $this->request->getUrl()->getScheme() . '://' .
756
+                            $requestHost . $requestUrl:
757
+                           $requestUrl;
758
+                foreach (array_keys(self::$challenges) as $key) {
759
+                    if ($key == substr($fullUrl, 0, strlen($key))) {
760
+                        $headers['authorization'] = $this->createDigestResponse(
761
+                            $auth['user'], $auth['password'],
762
+                            $requestUrl, self::$challenges[$key]
763
+                        );
764
+                        $this->serverChallenge =& self::$challenges[$key];
765
+                        break;
766
+                    }
767
+                }
768
+                break;
769
+
770
+            default:
771
+                throw new HTTP_Request2_NotImplementedException(
772
+                    "Unknown HTTP authentication scheme '{$auth['scheme']}'"
773
+                );
774
+        }
775
+    }
776
+
777
+   /**
778
+    * Adds 'Proxy-Authorization' header (if needed) to request headers array
779
+    *
780
+    * @param    array   request headers
781
+    * @param    string  request URL (needed for digest authentication)
782
+    * @throws   HTTP_Request2_NotImplementedException
783
+    */
784
+    protected function addProxyAuthorizationHeader(&$headers, $requestUrl)
785
+    {
786
+        if (!$this->request->getConfig('proxy_host') ||
787
+            !($user = $this->request->getConfig('proxy_user')) ||
788
+            (0 == strcasecmp('https', $this->request->getUrl()->getScheme()) &&
789
+             HTTP_Request2::METHOD_CONNECT != $this->request->getMethod())
790
+        ) {
791
+            return;
792
+        }
793
+
794
+        $password = $this->request->getConfig('proxy_password');
795
+        switch ($this->request->getConfig('proxy_auth_scheme')) {
796
+            case HTTP_Request2::AUTH_BASIC:
797
+                $headers['proxy-authorization'] =
798
+                    'Basic ' . base64_encode($user . ':' . $password);
799
+                break;
800
+
801
+            case HTTP_Request2::AUTH_DIGEST:
802
+                unset($this->proxyChallenge);
803
+                $proxyUrl = 'proxy://' . $this->request->getConfig('proxy_host') .
804
+                            ':' . $this->request->getConfig('proxy_port');
805
+                if (!empty(self::$challenges[$proxyUrl])) {
806
+                    $headers['proxy-authorization'] = $this->createDigestResponse(
807
+                        $user, $password,
808
+                        $requestUrl, self::$challenges[$proxyUrl]
809
+                    );
810
+                    $this->proxyChallenge =& self::$challenges[$proxyUrl];
811
+                }
812
+                break;
813
+
814
+            default:
815
+                throw new HTTP_Request2_NotImplementedException(
816
+                    "Unknown HTTP authentication scheme '" .
817
+                    $this->request->getConfig('proxy_auth_scheme') . "'"
818
+                );
819
+        }
820
+    }
821
+
822
+
823
+   /**
824
+    * Creates the string with the Request-Line and request headers
825
+    *
826
+    * @return   string
827
+    * @throws   HTTP_Request2_Exception
828
+    */
829
+    protected function prepareHeaders()
830
+    {
831
+        $headers = $this->request->getHeaders();
832
+        $url     = $this->request->getUrl();
833
+        $connect = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod();
834
+        $host    = $url->getHost();
835
+
836
+        $defaultPort = 0 == strcasecmp($url->getScheme(), 'https')? 443: 80;
837
+        if (($port = $url->getPort()) && $port != $defaultPort || $connect) {
838
+            $host .= ':' . (empty($port)? $defaultPort: $port);
839
+        }
840
+        // Do not overwrite explicitly set 'Host' header, see bug #16146
841
+        if (!isset($headers['host'])) {
842
+            $headers['host'] = $host;
843
+        }
844
+
845
+        if ($connect) {
846
+            $requestUrl = $host;
847
+
848
+        } else {
849
+            if (!$this->request->getConfig('proxy_host') ||
850
+                0 == strcasecmp($url->getScheme(), 'https')
851
+            ) {
852
+                $requestUrl = '';
853
+            } else {
854
+                $requestUrl = $url->getScheme() . '://' . $host;
855
+            }
856
+            $path        = $url->getPath();
857
+            $query       = $url->getQuery();
858
+            $requestUrl .= (empty($path)? '/': $path) . (empty($query)? '': '?' . $query);
859
+        }
860
+
861
+        if ('1.1' == $this->request->getConfig('protocol_version') &&
862
+            extension_loaded('zlib') && !isset($headers['accept-encoding'])
863
+        ) {
864
+            $headers['accept-encoding'] = 'gzip, deflate';
865
+        }
866
+        if (($jar = $this->request->getCookieJar())
867
+            && ($cookies = $jar->getMatching($this->request->getUrl(), true))
868
+        ) {
869
+            $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;
870
+        }
871
+
872
+        $this->addAuthorizationHeader($headers, $host, $requestUrl);
873
+        $this->addProxyAuthorizationHeader($headers, $requestUrl);
874
+        $this->calculateRequestLength($headers);
875
+
876
+        $headersStr = $this->request->getMethod() . ' ' . $requestUrl . ' HTTP/' .
877
+                      $this->request->getConfig('protocol_version') . "\r\n";
878
+        foreach ($headers as $name => $value) {
879
+            $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
880
+            $headersStr   .= $canonicalName . ': ' . $value . "\r\n";
881
+        }
882
+        return $headersStr . "\r\n";
883
+    }
884
+
885
+   /**
886
+    * Sends the request body
887
+    *
888
+    * @throws   HTTP_Request2_MessageException
889
+    */
890
+    protected function writeBody()
891
+    {
892
+        if (in_array($this->request->getMethod(), self::$bodyDisallowed) ||
893
+            0 == $this->contentLength
894
+        ) {
895
+            return;
896
+        }
897
+
898
+        $position   = 0;
899
+        $bufferSize = $this->request->getConfig('buffer_size');
900
+        while ($position < $this->contentLength) {
901
+            if (is_string($this->requestBody)) {
902
+                $str = substr($this->requestBody, $position, $bufferSize);
903
+            } elseif (is_resource($this->requestBody)) {
904
+                $str = fread($this->requestBody, $bufferSize);
905
+            } else {
906
+                $str = $this->requestBody->read($bufferSize);
907
+            }
908
+            if (false === @fwrite($this->socket, $str, strlen($str))) {
909
+                throw new HTTP_Request2_MessageException('Error writing request');
910
+            }
911
+            // Provide the length of written string to the observer, request #7630
912
+            $this->request->setLastEvent('sentBodyPart', strlen($str));
913
+            $position += strlen($str);
914
+        }
915
+        $this->request->setLastEvent('sentBody', $this->contentLength);
916
+    }
917
+
918
+   /**
919
+    * Reads the remote server's response
920
+    *
921
+    * @return   HTTP_Request2_Response
922
+    * @throws   HTTP_Request2_Exception
923
+    */
924
+    protected function readResponse()
925
+    {
926
+        $bufferSize = $this->request->getConfig('buffer_size');
927
+
928
+        do {
929
+            $response = new HTTP_Request2_Response(
930
+                $this->readLine($bufferSize), true, $this->request->getUrl()
931
+            );
932
+            do {
933
+                $headerLine = $this->readLine($bufferSize);
934
+                $response->parseHeaderLine($headerLine);
935
+            } while ('' != $headerLine);
936
+        } while (in_array($response->getStatus(), array(100, 101)));
937
+
938
+        $this->request->setLastEvent('receivedHeaders', $response);
939
+
940
+        // No body possible in such responses
941
+        if (HTTP_Request2::METHOD_HEAD == $this->request->getMethod() ||
942
+            (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod() &&
943
+             200 <= $response->getStatus() && 300 > $response->getStatus()) ||
944
+            in_array($response->getStatus(), array(204, 304))
945
+        ) {
946
+            return $response;
947
+        }
948
+
949
+        $chunked = 'chunked' == $response->getHeader('transfer-encoding');
950
+        $length  = $response->getHeader('content-length');
951
+        $hasBody = false;
952
+        if ($chunked || null === $length || 0 < intval($length)) {
953
+            // RFC 2616, section 4.4:
954
+            // 3. ... If a message is received with both a
955
+            // Transfer-Encoding header field and a Content-Length header field,
956
+            // the latter MUST be ignored.
957
+            $toRead = ($chunked || null === $length)? null: $length;
958
+            $this->chunkLength = 0;
959
+
960
+            while (!feof($this->socket) && (is_null($toRead) || 0 < $toRead)) {
961
+                if ($chunked) {
962
+                    $data = $this->readChunked($bufferSize);
963
+                } elseif (is_null($toRead)) {
964
+                    $data = $this->fread($bufferSize);
965
+                } else {
966
+                    $data    = $this->fread(min($toRead, $bufferSize));
967
+                    $toRead -= strlen($data);
968
+                }
969
+                if ('' == $data && (!$this->chunkLength || feof($this->socket))) {
970
+                    break;
971
+                }
972
+
973
+                $hasBody = true;
974
+                if ($this->request->getConfig('store_body')) {
975
+                    $response->appendBody($data);
976
+                }
977
+                if (!in_array($response->getHeader('content-encoding'), array('identity', null))) {
978
+                    $this->request->setLastEvent('receivedEncodedBodyPart', $data);
979
+                } else {
980
+                    $this->request->setLastEvent('receivedBodyPart', $data);
981
+                }
982
+            }
983
+        }
984
+
985
+        if ($hasBody) {
986
+            $this->request->setLastEvent('receivedBody', $response);
987
+        }
988
+        return $response;
989
+    }
990
+
991
+   /**
992
+    * Reads until either the end of the socket or a newline, whichever comes first
993
+    *
994
+    * Strips the trailing newline from the returned data, handles global
995
+    * request timeout. Method idea borrowed from Net_Socket PEAR package.
996
+    *
997
+    * @param    int     buffer size to use for reading
998
+    * @return   Available data up to the newline (not including newline)
999
+    * @throws   HTTP_Request2_MessageException     In case of timeout
1000
+    */
1001
+    protected function readLine($bufferSize)
1002
+    {
1003
+        $line = '';
1004
+        while (!feof($this->socket)) {
1005
+            if ($this->deadline) {
1006
+                stream_set_timeout($this->socket, max($this->deadline - time(), 1));
1007
+            }
1008
+            $line .= @fgets($this->socket, $bufferSize);
1009
+            $info  = stream_get_meta_data($this->socket);
1010
+            if ($info['timed_out'] || $this->deadline && time() > $this->deadline) {
1011
+                $reason = $this->deadline
1012
+                          ? 'after ' . $this->request->getConfig('timeout') . ' second(s)'
1013
+                          : 'due to default_socket_timeout php.ini setting';
1014
+                throw new HTTP_Request2_MessageException(
1015
+                    "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT
1016
+                );
1017
+            }
1018
+            if (substr($line, -1) == "\n") {
1019
+                return rtrim($line, "\r\n");
1020
+            }
1021
+        }
1022
+        return $line;
1023
+    }
1024
+
1025
+   /**
1026
+    * Wrapper around fread(), handles global request timeout
1027
+    *
1028
+    * @param    int     Reads up to this number of bytes
1029
+    * @return   Data read from socket
1030
+    * @throws   HTTP_Request2_MessageException     In case of timeout
1031
+    */
1032
+    protected function fread($length)
1033
+    {
1034
+        if ($this->deadline) {
1035
+            stream_set_timeout($this->socket, max($this->deadline - time(), 1));
1036
+        }
1037
+        $data = fread($this->socket, $length);
1038
+        $info = stream_get_meta_data($this->socket);
1039
+        if ($info['timed_out'] || $this->deadline && time() > $this->deadline) {
1040
+            $reason = $this->deadline
1041
+                      ? 'after ' . $this->request->getConfig('timeout') . ' second(s)'
1042
+                      : 'due to default_socket_timeout php.ini setting';
1043
+            throw new HTTP_Request2_MessageException(
1044
+                "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT
1045
+            );
1046
+        }
1047
+        return $data;
1048
+    }
1049
+
1050
+   /**
1051
+    * Reads a part of response body encoded with chunked Transfer-Encoding
1052
+    *
1053
+    * @param    int     buffer size to use for reading
1054
+    * @return   string
1055
+    * @throws   HTTP_Request2_MessageException
1056
+    */
1057
+    protected function readChunked($bufferSize)
1058
+    {
1059
+        // at start of the next chunk?
1060
+        if (0 == $this->chunkLength) {
1061
+            $line = $this->readLine($bufferSize);
1062
+            if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) {
1063
+                throw new HTTP_Request2_MessageException(
1064
+                    "Cannot decode chunked response, invalid chunk length '{$line}'",
1065
+                    HTTP_Request2_Exception::DECODE_ERROR
1066
+                );
1067
+            } else {
1068
+                $this->chunkLength = hexdec($matches[1]);
1069
+                // Chunk with zero length indicates the end
1070
+                if (0 == $this->chunkLength) {
1071
+                    $this->readLine($bufferSize);
1072
+                    return '';
1073
+                }
1074
+            }
1075
+        }
1076
+        $data = $this->fread(min($this->chunkLength, $bufferSize));
1077
+        $this->chunkLength -= strlen($data);
1078
+        if (0 == $this->chunkLength) {
1079
+            $this->readLine($bufferSize); // Trailing CRLF
1080
+        }
1081
+        return $data;
1082
+    }
1083
+}
1084
+
1085
+?>
1086
\ No newline at end of file
1087
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2/CookieJar.php Added
502
 
1
@@ -0,0 +1,499 @@
2
+<?php
3
+/**
4
+ * Stores cookies and passes them between HTTP requests
5
+ *
6
+ * PHP version 5
7
+ *
8
+ * LICENSE:
9
+ *
10
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>
11
+ * All rights reserved.
12
+ *
13
+ * Redistribution and use in source and binary forms, with or without
14
+ * modification, are permitted provided that the following conditions
15
+ * are met:
16
+ *
17
+ *    * Redistributions of source code must retain the above copyright
18
+ *      notice, this list of conditions and the following disclaimer.
19
+ *    * Redistributions in binary form must reproduce the above copyright
20
+ *      notice, this list of conditions and the following disclaimer in the
21
+ *      documentation and/or other materials provided with the distribution.
22
+ *    * The names of the authors may not be used to endorse or promote products
23
+ *      derived from this software without specific prior written permission.
24
+ *
25
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ *
37
+ * @category   HTTP
38
+ * @package    HTTP_Request2
39
+ * @author     Alexey Borzov <avb@php.net>
40
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
41
+ * @version    SVN: $Id: CookieJar.php 308629 2011-02-24 17:34:24Z avb $
42
+ * @link       http://pear.php.net/package/HTTP_Request2
43
+ */
44
+
45
+/** Class representing a HTTP request message */
46
+require_once 'HTTP/Request2.php';
47
+
48
+/**
49
+ * Stores cookies and passes them between HTTP requests
50
+ *
51
+ * @category   HTTP
52
+ * @package    HTTP_Request2
53
+ * @author     Alexey Borzov <avb@php.net>
54
+ * @version    Release: @package_version@
55
+ */
56
+class HTTP_Request2_CookieJar implements Serializable
57
+{
58
+   /**
59
+    * Array of stored cookies
60
+    *
61
+    * The array is indexed by domain, path and cookie name
62
+    *   .example.com
63
+    *     /
64
+    *       some_cookie => cookie data
65
+    *     /subdir
66
+    *       other_cookie => cookie data
67
+    *   .example.org
68
+    *     ...
69
+    *
70
+    * @var array
71
+    */
72
+    protected $cookies = array();
73
+
74
+   /**
75
+    * Whether session cookies should be serialized when serializing the jar
76
+    * @var bool
77
+    */
78
+    protected $serializeSession = false;
79
+
80
+   /**
81
+    * Whether Public Suffix List should be used for domain matching
82
+    * @var bool
83
+    */
84
+    protected $useList = true;
85
+
86
+   /**
87
+    * Array with Public Suffix List data
88
+    * @var  array
89
+    * @link http://publicsuffix.org/
90
+    */
91
+    protected static $psl = array();
92
+
93
+   /**
94
+    * Class constructor, sets various options
95
+    *
96
+    * @param bool Controls serializing session cookies, see {@link serializeSessionCookies()}
97
+    * @param bool Controls using Public Suffix List, see {@link usePublicSuffixList()}
98
+    */
99
+    public function __construct($serializeSessionCookies = false, $usePublicSuffixList = true)
100
+    {
101
+        $this->serializeSessionCookies($serializeSessionCookies);
102
+        $this->usePublicSuffixList($usePublicSuffixList);
103
+    }
104
+
105
+   /**
106
+    * Returns current time formatted in ISO-8601 at UTC timezone
107
+    *
108
+    * @return string
109
+    */
110
+    protected function now()
111
+    {
112
+        $dt = new DateTime();
113
+        $dt->setTimezone(new DateTimeZone('UTC'));
114
+        return $dt->format(DateTime::ISO8601);
115
+    }
116
+
117
+   /**
118
+    * Checks cookie array for correctness, possibly updating its 'domain', 'path' and 'expires' fields
119
+    *
120
+    * The checks are as follows:
121
+    *   - cookie array should contain 'name' and 'value' fields;
122
+    *   - name and value should not contain disallowed symbols;
123
+    *   - 'expires' should be either empty parseable by DateTime;
124
+    *   - 'domain' and 'path' should be either not empty or an URL where
125
+    *     cookie was set should be provided.
126
+    *   - if $setter is provided, then document at that URL should be allowed
127
+    *     to set a cookie for that 'domain'. If $setter is not provided,
128
+    *     then no domain checks will be made.
129
+    *
130
+    * 'expires' field will be converted to ISO8601 format from COOKIE format,
131
+    * 'domain' and 'path' will be set from setter URL if empty.
132
+    *
133
+    * @param    array    cookie data, as returned by {@link HTTP_Request2_Response::getCookies()}
134
+    * @param    Net_URL2 URL of the document that sent Set-Cookie header
135
+    * @return   array    Updated cookie array
136
+    * @throws   HTTP_Request2_LogicException
137
+    * @throws   HTTP_Request2_MessageException
138
+    */
139
+    protected function checkAndUpdateFields(array $cookie, Net_URL2 $setter = null)
140
+    {
141
+        if ($missing = array_diff(array('name', 'value'), array_keys($cookie))) {
142
+            throw new HTTP_Request2_LogicException(
143
+                "Cookie array should contain 'name' and 'value' fields",
144
+                HTTP_Request2_Exception::MISSING_VALUE
145
+            );
146
+        }
147
+        if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['name'])) {
148
+            throw new HTTP_Request2_LogicException(
149
+                "Invalid cookie name: '{$cookie['name']}'",
150
+                HTTP_Request2_Exception::INVALID_ARGUMENT
151
+            );
152
+        }
153
+        if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['value'])) {
154
+            throw new HTTP_Request2_LogicException(
155
+                "Invalid cookie value: '{$cookie['value']}'",
156
+                HTTP_Request2_Exception::INVALID_ARGUMENT
157
+            );
158
+        }
159
+        $cookie += array('domain' => '', 'path' => '', 'expires' => null, 'secure' => false);
160
+
161
+        // Need ISO-8601 date @ UTC timezone
162
+        if (!empty($cookie['expires'])
163
+            && !preg_match('/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+0000$/', $cookie['expires'])
164
+        ) {
165
+            try {
166
+                $dt = new DateTime($cookie['expires']);
167
+                $dt->setTimezone(new DateTimeZone('UTC'));
168
+                $cookie['expires'] = $dt->format(DateTime::ISO8601);
169
+            } catch (Exception $e) {
170
+                throw new HTTP_Request2_LogicException($e->getMessage());
171
+            }
172
+        }
173
+
174
+        if (empty($cookie['domain']) || empty($cookie['path'])) {
175
+            if (!$setter) {
176
+                throw new HTTP_Request2_LogicException(
177
+                    'Cookie misses domain and/or path component, cookie setter URL needed',
178
+                    HTTP_Request2_Exception::MISSING_VALUE
179
+                );
180
+            }
181
+            if (empty($cookie['domain'])) {
182
+                if ($host = $setter->getHost()) {
183
+                    $cookie['domain'] = $host;
184
+                } else {
185
+                    throw new HTTP_Request2_LogicException(
186
+                        'Setter URL does not contain host part, can\'t set cookie domain',
187
+                        HTTP_Request2_Exception::MISSING_VALUE
188
+                    );
189
+                }
190
+            }
191
+            if (empty($cookie['path'])) {
192
+                $path = $setter->getPath();
193
+                $cookie['path'] = empty($path)? '/': substr($path, 0, strrpos($path, '/') + 1);
194
+            }
195
+        }
196
+
197
+        if ($setter && !$this->domainMatch($setter->getHost(), $cookie['domain'])) {
198
+            throw new HTTP_Request2_MessageException(
199
+                "Domain " . $setter->getHost() . " cannot set cookies for "
200
+                . $cookie['domain']
201
+            );
202
+        }
203
+
204
+        return $cookie;
205
+    }
206
+
207
+   /**
208
+    * Stores a cookie in the jar
209
+    *
210
+    * @param    array    cookie data, as returned by {@link HTTP_Request2_Response::getCookies()}
211
+    * @param    Net_URL2 URL of the document that sent Set-Cookie header
212
+    * @throws   HTTP_Request2_Exception
213
+    */
214
+    public function store(array $cookie, Net_URL2 $setter = null)
215
+    {
216
+        $cookie = $this->checkAndUpdateFields($cookie, $setter);
217
+
218
+        if (strlen($cookie['value'])
219
+            && (is_null($cookie['expires']) || $cookie['expires'] > $this->now())
220
+        ) {
221
+            if (!isset($this->cookies[$cookie['domain']])) {
222
+                $this->cookies[$cookie['domain']] = array();
223
+            }
224
+            if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {
225
+                $this->cookies[$cookie['domain']][$cookie['path']] = array();
226
+            }
227
+            $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;
228
+
229
+        } elseif (isset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']])) {
230
+            unset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']]);
231
+        }
232
+    }
233
+
234
+   /**
235
+    * Adds cookies set in HTTP response to the jar
236
+    *
237
+    * @param HTTP_Request2_Response response
238
+    * @param Net_URL2               original request URL, needed for setting
239
+    *                               default domain/path
240
+    */
241
+    public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter)
242
+    {
243
+        foreach ($response->getCookies() as $cookie) {
244
+            $this->store($cookie, $setter);
245
+        }
246
+    }
247
+
248
+   /**
249
+    * Returns all cookies matching a given request URL
250
+    *
251
+    * The following checks are made:
252
+    *   - cookie domain should match request host
253
+    *   - cookie path should be a prefix for request path
254
+    *   - 'secure' cookies will only be sent for HTTPS requests
255
+    *
256
+    * @param  Net_URL2
257
+    * @param  bool      Whether to return cookies as string for "Cookie: " header
258
+    * @return array
259
+    */
260
+    public function getMatching(Net_URL2 $url, $asString = false)
261
+    {
262
+        $host   = $url->getHost();
263
+        $path   = $url->getPath();
264
+        $secure = 0 == strcasecmp($url->getScheme(), 'https');
265
+
266
+        $matched = $ret = array();
267
+        foreach (array_keys($this->cookies) as $domain) {
268
+            if ($this->domainMatch($host, $domain)) {
269
+                foreach (array_keys($this->cookies[$domain]) as $cPath) {
270
+                    if (0 === strpos($path, $cPath)) {
271
+                        foreach ($this->cookies[$domain][$cPath] as $name => $cookie) {
272
+                            if (!$cookie['secure'] || $secure) {
273
+                                $matched[$name][strlen($cookie['path'])] = $cookie;
274
+                            }
275
+                        }
276
+                    }
277
+                }
278
+            }
279
+        }
280
+        foreach ($matched as $cookies) {
281
+            krsort($cookies);
282
+            $ret = array_merge($ret, $cookies);
283
+        }
284
+        if (!$asString) {
285
+            return $ret;
286
+        } else {
287
+            $str = '';
288
+            foreach ($ret as $c) {
289
+                $str .= (empty($str)? '': '; ') . $c['name'] . '=' . $c['value'];
290
+            }
291
+            return $str;
292
+        }
293
+    }
294
+
295
+   /**
296
+    * Returns all cookies stored in a jar
297
+    *
298
+    * @return array
299
+    */
300
+    public function getAll()
301
+    {
302
+        $cookies = array();
303
+        foreach (array_keys($this->cookies) as $domain) {
304
+            foreach (array_keys($this->cookies[$domain]) as $path) {
305
+                foreach ($this->cookies[$domain][$path] as $name => $cookie) {
306
+                    $cookies[] = $cookie;
307
+                }
308
+            }
309
+        }
310
+        return $cookies;
311
+    }
312
+
313
+   /**
314
+    * Sets whether session cookies should be serialized when serializing the jar
315
+    *
316
+    * @param    boolean
317
+    */
318
+    public function serializeSessionCookies($serialize)
319
+    {
320
+        $this->serializeSession = (bool)$serialize;
321
+    }
322
+
323
+   /**
324
+    * Sets whether Public Suffix List should be used for restricting cookie-setting
325
+    *
326
+    * Without PSL {@link domainMatch()} will only prevent setting cookies for
327
+    * top-level domains like '.com' or '.org'. However, it will not prevent
328
+    * setting a cookie for '.co.uk' even though only third-level registrations
329
+    * are possible in .uk domain.
330
+    *
331
+    * With the List it is possible to find the highest level at which a domain
332
+    * may be registered for a particular top-level domain and consequently
333
+    * prevent cookies set for '.co.uk' or '.msk.ru'. The same list is used by
334
+    * Firefox, Chrome and Opera browsers to restrict cookie setting.
335
+    *
336
+    * Note that PSL is licensed differently to HTTP_Request2 package (refer to
337
+    * the license information in public-suffix-list.php), so you can disable
338
+    * its use if this is an issue for you.
339
+    *
340
+    * @param    boolean
341
+    * @link     http://publicsuffix.org/learn/
342
+    */
343
+    public function usePublicSuffixList($useList)
344
+    {
345
+        $this->useList = (bool)$useList;
346
+    }
347
+
348
+   /**
349
+    * Returns string representation of object
350
+    *
351
+    * @return string
352
+    * @see    Serializable::serialize()
353
+    */
354
+    public function serialize()
355
+    {
356
+        $cookies = $this->getAll();
357
+        if (!$this->serializeSession) {
358
+            for ($i = count($cookies) - 1; $i >= 0; $i--) {
359
+                if (empty($cookies[$i]['expires'])) {
360
+                    unset($cookies[$i]);
361
+                }
362
+            }
363
+        }
364
+        return serialize(array(
365
+            'cookies'          => $cookies,
366
+            'serializeSession' => $this->serializeSession,
367
+            'useList'          => $this->useList
368
+        ));
369
+    }
370
+
371
+   /**
372
+    * Constructs the object from serialized string
373
+    *
374
+    * @param string  string representation
375
+    * @see   Serializable::unserialize()
376
+    */
377
+    public function unserialize($serialized)
378
+    {
379
+        $data = unserialize($serialized);
380
+        $now  = $this->now();
381
+        $this->serializeSessionCookies($data['serializeSession']);
382
+        $this->usePublicSuffixList($data['useList']);
383
+        foreach ($data['cookies'] as $cookie) {
384
+            if (!empty($cookie['expires']) && $cookie['expires'] <= $now) {
385
+                continue;
386
+            }
387
+            if (!isset($this->cookies[$cookie['domain']])) {
388
+                $this->cookies[$cookie['domain']] = array();
389
+            }
390
+            if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) {
391
+                $this->cookies[$cookie['domain']][$cookie['path']] = array();
392
+            }
393
+            $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie;
394
+        }
395
+    }
396
+
397
+   /**
398
+    * Checks whether a cookie domain matches a request host.
399
+    *
400
+    * The method is used by {@link store()} to check for whether a document
401
+    * at given URL can set a cookie with a given domain attribute and by
402
+    * {@link getMatching()} to find cookies matching the request URL.
403
+    *
404
+    * @param    string  request host
405
+    * @param    string  cookie domain
406
+    * @return   bool    match success
407
+    */
408
+    public function domainMatch($requestHost, $cookieDomain)
409
+    {
410
+        if ($requestHost == $cookieDomain) {
411
+            return true;
412
+        }
413
+        // IP address, we require exact match
414
+        if (preg_match('/^(?:\d{1,3}\.){3}\d{1,3}$/', $requestHost)) {
415
+            return false;
416
+        }
417
+        if ('.' != $cookieDomain[0]) {
418
+            $cookieDomain = '.' . $cookieDomain;
419
+        }
420
+        // prevents setting cookies for '.com' and similar domains
421
+        if (!$this->useList && substr_count($cookieDomain, '.') < 2
422
+            || $this->useList && !self::getRegisteredDomain($cookieDomain)
423
+        ) {
424
+            return false;
425
+        }
426
+        return substr('.' . $requestHost, -strlen($cookieDomain)) == $cookieDomain;
427
+    }
428
+
429
+   /**
430
+    * Removes subdomains to get the registered domain (the first after top-level)
431
+    *
432
+    * The method will check Public Suffix List to find out where top-level
433
+    * domain ends and registered domain starts. It will remove domain parts
434
+    * to the left of registered one.
435
+    *
436
+    * @param  string        domain name
437
+    * @return string|bool   registered domain, will return false if $domain is
438
+    *                       either invalid or a TLD itself
439
+    */
440
+    public static function getRegisteredDomain($domain)
441
+    {
442
+        $domainParts = explode('.', ltrim($domain, '.'));
443
+
444
+        // load the list if needed
445
+        if (empty(self::$psl)) {
446
+            $path = '@data_dir@' . DIRECTORY_SEPARATOR . 'HTTP_Request2';
447
+            if (0 === strpos($path, '@' . 'data_dir@')) {
448
+                $path = realpath(dirname(__FILE__) . DIRECTORY_SEPARATOR . '..'
449
+                                 . DIRECTORY_SEPARATOR . 'data');
450
+            }
451
+            self::$psl = include_once $path . DIRECTORY_SEPARATOR . 'public-suffix-list.php';
452
+        }
453
+
454
+        if (!($result = self::checkDomainsList($domainParts, self::$psl))) {
455
+            // known TLD, invalid domain name
456
+            return false;
457
+        }
458
+
459
+        // unknown TLD
460
+        if (!strpos($result, '.')) {
461
+            // fallback to checking that domain "has at least two dots"
462
+            if (2 > ($count = count($domainParts))) {
463
+                return false;
464
+            }
465
+            return $domainParts[$count - 2] . '.' . $domainParts[$count - 1];
466
+        }
467
+        return $result;
468
+    }
469
+
470
+   /**
471
+    * Recursive helper method for {@link getRegisteredDomain()}
472
+    *
473
+    * @param  array         remaining domain parts
474
+    * @param  mixed         node in {@link HTTP_Request2_CookieJar::$psl} to check
475
+    * @return string|null   concatenated domain parts, null in case of error
476
+    */
477
+    protected static function checkDomainsList(array $domainParts, $listNode)
478
+    {
479
+        $sub    = array_pop($domainParts);
480
+        $result = null;
481
+
482
+        if (!is_array($listNode) || is_null($sub)
483
+            || array_key_exists('!' . $sub, $listNode)
484
+         ) {
485
+            return $sub;
486
+
487
+        } elseif (array_key_exists($sub, $listNode)) {
488
+            $result = self::checkDomainsList($domainParts, $listNode[$sub]);
489
+
490
+        } elseif (array_key_exists('*', $listNode)) {
491
+            $result = self::checkDomainsList($domainParts, $listNode['*']);
492
+
493
+        } else {
494
+            return $sub;
495
+        }
496
+
497
+        return (strlen($result) > 0) ? ($result . '.' . $sub) : null;
498
+    }
499
+}
500
+?>
501
\ No newline at end of file
502
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2/Exception.php Added
163
 
1
@@ -0,0 +1,160 @@
2
+<?php
3
+/**
4
+ * Exception classes for HTTP_Request2 package
5
+ *
6
+ * PHP version 5
7
+ *
8
+ * LICENSE:
9
+ *
10
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>
11
+ * All rights reserved.
12
+ *
13
+ * Redistribution and use in source and binary forms, with or without
14
+ * modification, are permitted provided that the following conditions
15
+ * are met:
16
+ *
17
+ *    * Redistributions of source code must retain the above copyright
18
+ *      notice, this list of conditions and the following disclaimer.
19
+ *    * Redistributions in binary form must reproduce the above copyright
20
+ *      notice, this list of conditions and the following disclaimer in the
21
+ *      documentation and/or other materials provided with the distribution.
22
+ *    * The names of the authors may not be used to endorse or promote products
23
+ *      derived from this software without specific prior written permission.
24
+ *
25
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ *
37
+ * @category   HTTP
38
+ * @package    HTTP_Request2
39
+ * @author     Alexey Borzov <avb@php.net>
40
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
41
+ * @version    SVN: $Id: Exception.php 308629 2011-02-24 17:34:24Z avb $
42
+ * @link       http://pear.php.net/package/HTTP_Request2
43
+ */
44
+
45
+/**
46
+ * Base class for exceptions in PEAR
47
+ */
48
+require_once 'PEAR/Exception.php';
49
+
50
+/**
51
+ * Base exception class for HTTP_Request2 package
52
+ *
53
+ * @category   HTTP
54
+ * @package    HTTP_Request2
55
+ * @version    Release: 2.0.0
56
+ * @link       http://pear.php.net/pepr/pepr-proposal-show.php?id=132
57
+ */
58
+class HTTP_Request2_Exception extends PEAR_Exception
59
+{
60
+    /** An invalid argument was passed to a method */
61
+    const INVALID_ARGUMENT   = 1;
62
+    /** Some required value was not available */
63
+    const MISSING_VALUE      = 2;
64
+    /** Request cannot be processed due to errors in PHP configuration */
65
+    const MISCONFIGURATION   = 3;
66
+    /** Error reading the local file */
67
+    const READ_ERROR         = 4;
68
+
69
+    /** Server returned a response that does not conform to HTTP protocol */
70
+    const MALFORMED_RESPONSE = 10;
71
+    /** Failure decoding Content-Encoding or Transfer-Encoding of response */
72
+    const DECODE_ERROR       = 20;
73
+    /** Operation timed out */
74
+    const TIMEOUT            = 30;
75
+    /** Number of redirects exceeded 'max_redirects' configuration parameter */
76
+    const TOO_MANY_REDIRECTS = 40;
77
+    /** Redirect to a protocol other than http(s):// */
78
+    const NON_HTTP_REDIRECT  = 50;
79
+
80
+   /**
81
+    * Native error code
82
+    * @var int
83
+    */
84
+    private $_nativeCode;
85
+
86
+   /**
87
+    * Constructor, can set package error code and native error code
88
+    *
89
+    * @param string exception message
90
+    * @param int    package error code, one of class constants
91
+    * @param int    error code from underlying PHP extension
92
+    */
93
+    public function __construct($message = null, $code = null, $nativeCode = null)
94
+    {
95
+        parent::__construct($message, $code);
96
+        $this->_nativeCode = $nativeCode;
97
+    }
98
+
99
+   /**
100
+    * Returns error code produced by underlying PHP extension
101
+    *
102
+    * For Socket Adapter this may contain error number returned by
103
+    * stream_socket_client(), for Curl Adapter this will contain error number
104
+    * returned by curl_errno()
105
+    *
106
+    * @return integer
107
+    */
108
+    public function getNativeCode()
109
+    {
110
+        return $this->_nativeCode;
111
+    }
112
+}
113
+
114
+/**
115
+ * Exception thrown in case of missing features
116
+ *
117
+ * @category   HTTP
118
+ * @package    HTTP_Request2
119
+ * @version    Release: 2.0.0
120
+ */
121
+class HTTP_Request2_NotImplementedException extends HTTP_Request2_Exception {}
122
+
123
+/**
124
+ * Exception that represents error in the program logic
125
+ *
126
+ * This exception usually implies a programmer's error, like passing invalid
127
+ * data to methods or trying to use PHP extensions that weren't installed or
128
+ * enabled. Usually exceptions of this kind will be thrown before request even
129
+ * starts.
130
+ *
131
+ * The exception will usually contain a package error code.
132
+ *
133
+ * @category   HTTP
134
+ * @package    HTTP_Request2
135
+ * @version    Release: 2.0.0
136
+ */
137
+class HTTP_Request2_LogicException extends HTTP_Request2_Exception {}
138
+
139
+/**
140
+ * Exception thrown when connection to a web or proxy server fails
141
+ *
142
+ * The exception will not contain a package error code, but will contain
143
+ * native error code, as returned by stream_socket_client() or curl_errno().
144
+ *
145
+ * @category   HTTP
146
+ * @package    HTTP_Request2
147
+ * @version    Release: 2.0.0
148
+ */
149
+class HTTP_Request2_ConnectionException extends HTTP_Request2_Exception {}
150
+
151
+/**
152
+ * Exception thrown when sending or receiving HTTP message fails
153
+ *
154
+ * The exception may contain both package error code and native error code.
155
+ *
156
+ * @category   HTTP
157
+ * @package    HTTP_Request2
158
+ * @version    Release: 2.0.0
159
+ */
160
+class HTTP_Request2_MessageException extends HTTP_Request2_Exception {}
161
+?>
162
\ No newline at end of file
163
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2/MultipartBody.php Added
276
 
1
@@ -0,0 +1,274 @@
2
+<?php
3
+/**
4
+ * Helper class for building multipart/form-data request body
5
+ *
6
+ * PHP version 5
7
+ *
8
+ * LICENSE:
9
+ *
10
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>
11
+ * All rights reserved.
12
+ *
13
+ * Redistribution and use in source and binary forms, with or without
14
+ * modification, are permitted provided that the following conditions
15
+ * are met:
16
+ *
17
+ *    * Redistributions of source code must retain the above copyright
18
+ *      notice, this list of conditions and the following disclaimer.
19
+ *    * Redistributions in binary form must reproduce the above copyright
20
+ *      notice, this list of conditions and the following disclaimer in the
21
+ *      documentation and/or other materials provided with the distribution.
22
+ *    * The names of the authors may not be used to endorse or promote products
23
+ *      derived from this software without specific prior written permission.
24
+ *
25
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ *
37
+ * @category   HTTP
38
+ * @package    HTTP_Request2
39
+ * @author     Alexey Borzov <avb@php.net>
40
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
41
+ * @version    SVN: $Id: MultipartBody.php 308322 2011-02-14 13:58:03Z avb $
42
+ * @link       http://pear.php.net/package/HTTP_Request2
43
+ */
44
+
45
+/**
46
+ * Class for building multipart/form-data request body
47
+ *
48
+ * The class helps to reduce memory consumption by streaming large file uploads
49
+ * from disk, it also allows monitoring of upload progress (see request #7630)
50
+ *
51
+ * @category   HTTP
52
+ * @package    HTTP_Request2
53
+ * @author     Alexey Borzov <avb@php.net>
54
+ * @version    Release: 2.0.0
55
+ * @link       http://tools.ietf.org/html/rfc1867
56
+ */
57
+class HTTP_Request2_MultipartBody
58
+{
59
+   /**
60
+    * MIME boundary
61
+    * @var  string
62
+    */
63
+    private $_boundary;
64
+
65
+   /**
66
+    * Form parameters added via {@link HTTP_Request2::addPostParameter()}
67
+    * @var  array
68
+    */
69
+    private $_params = array();
70
+
71
+   /**
72
+    * File uploads added via {@link HTTP_Request2::addUpload()}
73
+    * @var  array
74
+    */
75
+    private $_uploads = array();
76
+
77
+   /**
78
+    * Header for parts with parameters
79
+    * @var  string
80
+    */
81
+    private $_headerParam = "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n";
82
+
83
+   /**
84
+    * Header for parts with uploads
85
+    * @var  string
86
+    */
87
+    private $_headerUpload = "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n";
88
+
89
+   /**
90
+    * Current position in parameter and upload arrays
91
+    *
92
+    * First number is index of "current" part, second number is position within
93
+    * "current" part
94
+    *
95
+    * @var  array
96
+    */
97
+    private $_pos = array(0, 0);
98
+
99
+
100
+   /**
101
+    * Constructor. Sets the arrays with POST data.
102
+    *
103
+    * @param    array   values of form fields set via {@link HTTP_Request2::addPostParameter()}
104
+    * @param    array   file uploads set via {@link HTTP_Request2::addUpload()}
105
+    * @param    bool    whether to append brackets to array variable names
106
+    */
107
+    public function __construct(array $params, array $uploads, $useBrackets = true)
108
+    {
109
+        $this->_params = self::_flattenArray('', $params, $useBrackets);
110
+        foreach ($uploads as $fieldName => $f) {
111
+            if (!is_array($f['fp'])) {
112
+                $this->_uploads[] = $f + array('name' => $fieldName);
113
+            } else {
114
+                for ($i = 0; $i < count($f['fp']); $i++) {
115
+                    $upload = array(
116
+                        'name' => ($useBrackets? $fieldName . '[' . $i . ']': $fieldName)
117
+                    );
118
+                    foreach (array('fp', 'filename', 'size', 'type') as $key) {
119
+                        $upload[$key] = $f[$key][$i];
120
+                    }
121
+                    $this->_uploads[] = $upload;
122
+                }
123
+            }
124
+        }
125
+    }
126
+
127
+   /**
128
+    * Returns the length of the body to use in Content-Length header
129
+    *
130
+    * @return   integer
131
+    */
132
+    public function getLength()
133
+    {
134
+        $boundaryLength     = strlen($this->getBoundary());
135
+        $headerParamLength  = strlen($this->_headerParam) - 4 + $boundaryLength;
136
+        $headerUploadLength = strlen($this->_headerUpload) - 8 + $boundaryLength;
137
+        $length             = $boundaryLength + 6;
138
+        foreach ($this->_params as $p) {
139
+            $length += $headerParamLength + strlen($p[0]) + strlen($p[1]) + 2;
140
+        }
141
+        foreach ($this->_uploads as $u) {
142
+            $length += $headerUploadLength + strlen($u['name']) + strlen($u['type']) +
143
+                       strlen($u['filename']) + $u['size'] + 2;
144
+        }
145
+        return $length;
146
+    }
147
+
148
+   /**
149
+    * Returns the boundary to use in Content-Type header
150
+    *
151
+    * @return   string
152
+    */
153
+    public function getBoundary()
154
+    {
155
+        if (empty($this->_boundary)) {
156
+            $this->_boundary = '--' . md5('PEAR-HTTP_Request2-' . microtime());
157
+        }
158
+        return $this->_boundary;
159
+    }
160
+
161
+   /**
162
+    * Returns next chunk of request body
163
+    *
164
+    * @param    integer Amount of bytes to read
165
+    * @return   string  Up to $length bytes of data, empty string if at end
166
+    */
167
+    public function read($length)
168
+    {
169
+        $ret         = '';
170
+        $boundary    = $this->getBoundary();
171
+        $paramCount  = count($this->_params);
172
+        $uploadCount = count($this->_uploads);
173
+        while ($length > 0 && $this->_pos[0] <= $paramCount + $uploadCount) {
174
+            $oldLength = $length;
175
+            if ($this->_pos[0] < $paramCount) {
176
+                $param = sprintf($this->_headerParam, $boundary,
177
+                                 $this->_params[$this->_pos[0]][0]) .
178
+                         $this->_params[$this->_pos[0]][1] . "\r\n";
179
+                $ret    .= substr($param, $this->_pos[1], $length);
180
+                $length -= min(strlen($param) - $this->_pos[1], $length);
181
+
182
+            } elseif ($this->_pos[0] < $paramCount + $uploadCount) {
183
+                $pos    = $this->_pos[0] - $paramCount;
184
+                $header = sprintf($this->_headerUpload, $boundary,
185
+                                  $this->_uploads[$pos]['name'],
186
+                                  $this->_uploads[$pos]['filename'],
187
+                                  $this->_uploads[$pos]['type']);
188
+                if ($this->_pos[1] < strlen($header)) {
189
+                    $ret    .= substr($header, $this->_pos[1], $length);
190
+                    $length -= min(strlen($header) - $this->_pos[1], $length);
191
+                }
192
+                $filePos  = max(0, $this->_pos[1] - strlen($header));
193
+                if ($length > 0 && $filePos < $this->_uploads[$pos]['size']) {
194
+                    $ret     .= fread($this->_uploads[$pos]['fp'], $length);
195
+                    $length  -= min($length, $this->_uploads[$pos]['size'] - $filePos);
196
+                }
197
+                if ($length > 0) {
198
+                    $start   = $this->_pos[1] + ($oldLength - $length) -
199
+                               strlen($header) - $this->_uploads[$pos]['size'];
200
+                    $ret    .= substr("\r\n", $start, $length);
201
+                    $length -= min(2 - $start, $length);
202
+                }
203
+
204
+            } else {
205
+                $closing  = '--' . $boundary . "--\r\n";
206
+                $ret     .= substr($closing, $this->_pos[1], $length);
207
+                $length  -= min(strlen($closing) - $this->_pos[1], $length);
208
+            }
209
+            if ($length > 0) {
210
+                $this->_pos     = array($this->_pos[0] + 1, 0);
211
+            } else {
212
+                $this->_pos[1] += $oldLength;
213
+            }
214
+        }
215
+        return $ret;
216
+    }
217
+
218
+   /**
219
+    * Sets the current position to the start of the body
220
+    *
221
+    * This allows reusing the same body in another request
222
+    */
223
+    public function rewind()
224
+    {
225
+        $this->_pos = array(0, 0);
226
+        foreach ($this->_uploads as $u) {
227
+            rewind($u['fp']);
228
+        }
229
+    }
230
+
231
+   /**
232
+    * Returns the body as string
233
+    *
234
+    * Note that it reads all file uploads into memory so it is a good idea not
235
+    * to use this method with large file uploads and rely on read() instead.
236
+    *
237
+    * @return   string
238
+    */
239
+    public function __toString()
240
+    {
241
+        $this->rewind();
242
+        return $this->read($this->getLength());
243
+    }
244
+
245
+
246
+   /**
247
+    * Helper function to change the (probably multidimensional) associative array
248
+    * into the simple one.
249
+    *
250
+    * @param    string  name for item
251
+    * @param    mixed   item's values
252
+    * @param    bool    whether to append [] to array variables' names
253
+    * @return   array   array with the following items: array('item name', 'item value');
254
+    */
255
+    private static function _flattenArray($name, $values, $useBrackets)
256
+    {
257
+        if (!is_array($values)) {
258
+            return array(array($name, $values));
259
+        } else {
260
+            $ret = array();
261
+            foreach ($values as $k => $v) {
262
+                if (empty($name)) {
263
+                    $newName = $k;
264
+                } elseif ($useBrackets) {
265
+                    $newName = $name . '[' . $k . ']';
266
+                } else {
267
+                    $newName = $name;
268
+                }
269
+                $ret = array_merge($ret, self::_flattenArray($newName, $v, $useBrackets));
270
+            }
271
+            return $ret;
272
+        }
273
+    }
274
+}
275
+?>
276
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2/Observer Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2/Observer/Log.php Added
218
 
1
@@ -0,0 +1,215 @@
2
+<?php
3
+/**
4
+ * An observer useful for debugging / testing.
5
+ *
6
+ * PHP version 5
7
+ *
8
+ * LICENSE:
9
+ *
10
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>
11
+ * All rights reserved.
12
+ *
13
+ * Redistribution and use in source and binary forms, with or without
14
+ * modification, are permitted provided that the following conditions
15
+ * are met:
16
+ *
17
+ *    * Redistributions of source code must retain the above copyright
18
+ *      notice, this list of conditions and the following disclaimer.
19
+ *    * Redistributions in binary form must reproduce the above copyright
20
+ *      notice, this list of conditions and the following disclaimer in the
21
+ *      documentation and/or other materials provided with the distribution.
22
+ *    * The names of the authors may not be used to endorse or promote products
23
+ *      derived from this software without specific prior written permission.
24
+ *
25
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ *
37
+ * @category HTTP
38
+ * @package  HTTP_Request2
39
+ * @author   David Jean Louis <izi@php.net>
40
+ * @author   Alexey Borzov <avb@php.net>
41
+ * @license  http://opensource.org/licenses/bsd-license.php New BSD License
42
+ * @version  SVN: $Id: Log.php 308680 2011-02-25 17:40:17Z avb $
43
+ * @link     http://pear.php.net/package/HTTP_Request2
44
+ */
45
+
46
+/**
47
+ * Exception class for HTTP_Request2 package
48
+ */
49
+require_once 'HTTP/Request2/Exception.php';
50
+
51
+/**
52
+ * A debug observer useful for debugging / testing.
53
+ *
54
+ * This observer logs to a log target data corresponding to the various request
55
+ * and response events, it logs by default to php://output but can be configured
56
+ * to log to a file or via the PEAR Log package.
57
+ *
58
+ * A simple example:
59
+ * <code>
60
+ * require_once 'HTTP/Request2.php';
61
+ * require_once 'HTTP/Request2/Observer/Log.php';
62
+ *
63
+ * $request  = new HTTP_Request2('http://www.example.com');
64
+ * $observer = new HTTP_Request2_Observer_Log();
65
+ * $request->attach($observer);
66
+ * $request->send();
67
+ * </code>
68
+ *
69
+ * A more complex example with PEAR Log:
70
+ * <code>
71
+ * require_once 'HTTP/Request2.php';
72
+ * require_once 'HTTP/Request2/Observer/Log.php';
73
+ * require_once 'Log.php';
74
+ *
75
+ * $request  = new HTTP_Request2('http://www.example.com');
76
+ * // we want to log with PEAR log
77
+ * $observer = new HTTP_Request2_Observer_Log(Log::factory('console'));
78
+ *
79
+ * // we only want to log received headers
80
+ * $observer->events = array('receivedHeaders');
81
+ *
82
+ * $request->attach($observer);
83
+ * $request->send();
84
+ * </code>
85
+ *
86
+ * @category HTTP
87
+ * @package  HTTP_Request2
88
+ * @author   David Jean Louis <izi@php.net>
89
+ * @author   Alexey Borzov <avb@php.net>
90
+ * @license  http://opensource.org/licenses/bsd-license.php New BSD License
91
+ * @version  Release: 2.0.0
92
+ * @link     http://pear.php.net/package/HTTP_Request2
93
+ */
94
+class HTTP_Request2_Observer_Log implements SplObserver
95
+{
96
+    // properties {{{
97
+
98
+    /**
99
+     * The log target, it can be a a resource or a PEAR Log instance.
100
+     *
101
+     * @var resource|Log $target
102
+     */
103
+    protected $target = null;
104
+
105
+    /**
106
+     * The events to log.
107
+     *
108
+     * @var array $events
109
+     */
110
+    public $events = array(
111
+        'connect',
112
+        'sentHeaders',
113
+        'sentBody',
114
+        'receivedHeaders',
115
+        'receivedBody',
116
+        'disconnect',
117
+    );
118
+
119
+    // }}}
120
+    // __construct() {{{
121
+
122
+    /**
123
+     * Constructor.
124
+     *
125
+     * @param mixed $target Can be a file path (default: php://output), a resource,
126
+     *                      or an instance of the PEAR Log class.
127
+     * @param array $events Array of events to listen to (default: all events)
128
+     *
129
+     * @return void
130
+     */
131
+    public function __construct($target = 'php://output', array $events = array())
132
+    {
133
+        if (!empty($events)) {
134
+            $this->events = $events;
135
+        }
136
+        if (is_resource($target) || $target instanceof Log) {
137
+            $this->target = $target;
138
+        } elseif (false === ($this->target = @fopen($target, 'ab'))) {
139
+            throw new HTTP_Request2_Exception("Unable to open '{$target}'");
140
+        }
141
+    }
142
+
143
+    // }}}
144
+    // update() {{{
145
+
146
+    /**
147
+     * Called when the request notifies us of an event.
148
+     *
149
+     * @param HTTP_Request2 $subject The HTTP_Request2 instance
150
+     *
151
+     * @return void
152
+     */
153
+    public function update(SplSubject $subject)
154
+    {
155
+        $event = $subject->getLastEvent();
156
+        if (!in_array($event['name'], $this->events)) {
157
+            return;
158
+        }
159
+
160
+        switch ($event['name']) {
161
+        case 'connect':
162
+            $this->log('* Connected to ' . $event['data']);
163
+            break;
164
+        case 'sentHeaders':
165
+            $headers = explode("\r\n", $event['data']);
166
+            array_pop($headers);
167
+            foreach ($headers as $header) {
168
+                $this->log('> ' . $header);
169
+            }
170
+            break;
171
+        case 'sentBody':
172
+            $this->log('> ' . $event['data'] . ' byte(s) sent');
173
+            break;
174
+        case 'receivedHeaders':
175
+            $this->log(sprintf('< HTTP/%s %s %s',
176
+                $event['data']->getVersion(),
177
+                $event['data']->getStatus(),
178
+                $event['data']->getReasonPhrase()));
179
+            $headers = $event['data']->getHeader();
180
+            foreach ($headers as $key => $val) {
181
+                $this->log('< ' . $key . ': ' . $val);
182
+            }
183
+            $this->log('< ');
184
+            break;
185
+        case 'receivedBody':
186
+            $this->log($event['data']->getBody());
187
+            break;
188
+        case 'disconnect':
189
+            $this->log('* Disconnected');
190
+            break;
191
+        }
192
+    }
193
+
194
+    // }}}
195
+    // log() {{{
196
+
197
+    /**
198
+     * Logs the given message to the configured target.
199
+     *
200
+     * @param string $message Message to display
201
+     *
202
+     * @return void
203
+     */
204
+    protected function log($message)
205
+    {
206
+        if ($this->target instanceof Log) {
207
+            $this->target->debug($message);
208
+        } elseif (is_resource($this->target)) {
209
+            fwrite($this->target, $message . "\r\n");
210
+        }
211
+    }
212
+
213
+    // }}}
214
+}
215
+
216
+?>
217
\ No newline at end of file
218
iRony-0.4.4.tar.gz/lib/FileAPI/ext/HTTP/Request2/Response.php Added
646
 
1
@@ -0,0 +1,643 @@
2
+<?php
3
+/**
4
+ * Class representing a HTTP response
5
+ *
6
+ * PHP version 5
7
+ *
8
+ * LICENSE:
9
+ *
10
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>
11
+ * All rights reserved.
12
+ *
13
+ * Redistribution and use in source and binary forms, with or without
14
+ * modification, are permitted provided that the following conditions
15
+ * are met:
16
+ *
17
+ *    * Redistributions of source code must retain the above copyright
18
+ *      notice, this list of conditions and the following disclaimer.
19
+ *    * Redistributions in binary form must reproduce the above copyright
20
+ *      notice, this list of conditions and the following disclaimer in the
21
+ *      documentation and/or other materials provided with the distribution.
22
+ *    * The names of the authors may not be used to endorse or promote products
23
+ *      derived from this software without specific prior written permission.
24
+ *
25
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ *
37
+ * @category   HTTP
38
+ * @package    HTTP_Request2
39
+ * @author     Alexey Borzov <avb@php.net>
40
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
41
+ * @version    SVN: $Id: Response.php 317591 2011-10-01 08:37:49Z avb $
42
+ * @link       http://pear.php.net/package/HTTP_Request2
43
+ */
44
+
45
+/**
46
+ * Exception class for HTTP_Request2 package
47
+ */
48
+require_once 'HTTP/Request2/Exception.php';
49
+
50
+/**
51
+ * Class representing a HTTP response
52
+ *
53
+ * The class is designed to be used in "streaming" scenario, building the
54
+ * response as it is being received:
55
+ * <code>
56
+ * $statusLine = read_status_line();
57
+ * $response = new HTTP_Request2_Response($statusLine);
58
+ * do {
59
+ *     $headerLine = read_header_line();
60
+ *     $response->parseHeaderLine($headerLine);
61
+ * } while ($headerLine != '');
62
+ *
63
+ * while ($chunk = read_body()) {
64
+ *     $response->appendBody($chunk);
65
+ * }
66
+ *
67
+ * var_dump($response->getHeader(), $response->getCookies(), $response->getBody());
68
+ * </code>
69
+ *
70
+ *
71
+ * @category   HTTP
72
+ * @package    HTTP_Request2
73
+ * @author     Alexey Borzov <avb@php.net>
74
+ * @version    Release: 2.0.0
75
+ * @link       http://tools.ietf.org/html/rfc2616#section-6
76
+ */
77
+class HTTP_Request2_Response
78
+{
79
+   /**
80
+    * HTTP protocol version (e.g. 1.0, 1.1)
81
+    * @var  string
82
+    */
83
+    protected $version;
84
+
85
+   /**
86
+    * Status code
87
+    * @var  integer
88
+    * @link http://tools.ietf.org/html/rfc2616#section-6.1.1
89
+    */
90
+    protected $code;
91
+
92
+   /**
93
+    * Reason phrase
94
+    * @var  string
95
+    * @link http://tools.ietf.org/html/rfc2616#section-6.1.1
96
+    */
97
+    protected $reasonPhrase;
98
+
99
+   /**
100
+    * Effective URL (may be different from original request URL in case of redirects)
101
+    * @var  string
102
+    */
103
+    protected $effectiveUrl;
104
+
105
+   /**
106
+    * Associative array of response headers
107
+    * @var  array
108
+    */
109
+    protected $headers = array();
110
+
111
+   /**
112
+    * Cookies set in the response
113
+    * @var  array
114
+    */
115
+    protected $cookies = array();
116
+
117
+   /**
118
+    * Name of last header processed by parseHederLine()
119
+    *
120
+    * Used to handle the headers that span multiple lines
121
+    *
122
+    * @var  string
123
+    */
124
+    protected $lastHeader = null;
125
+
126
+   /**
127
+    * Response body
128
+    * @var  string
129
+    */
130
+    protected $body = '';
131
+
132
+   /**
133
+    * Whether the body is still encoded by Content-Encoding
134
+    *
135
+    * cURL provides the decoded body to the callback; if we are reading from
136
+    * socket the body is still gzipped / deflated
137
+    *
138
+    * @var  bool
139
+    */
140
+    protected $bodyEncoded;
141
+
142
+   /**
143
+    * Associative array of HTTP status code / reason phrase.
144
+    *
145
+    * @var  array
146
+    * @link http://tools.ietf.org/html/rfc2616#section-10
147
+    */
148
+    protected static $phrases = array(
149
+
150
+        // 1xx: Informational - Request received, continuing process
151
+        100 => 'Continue',
152
+        101 => 'Switching Protocols',
153
+
154
+        // 2xx: Success - The action was successfully received, understood and
155
+        // accepted
156
+        200 => 'OK',
157
+        201 => 'Created',
158
+        202 => 'Accepted',
159
+        203 => 'Non-Authoritative Information',
160
+        204 => 'No Content',
161
+        205 => 'Reset Content',
162
+        206 => 'Partial Content',
163
+
164
+        // 3xx: Redirection - Further action must be taken in order to complete
165
+        // the request
166
+        300 => 'Multiple Choices',
167
+        301 => 'Moved Permanently',
168
+        302 => 'Found',  // 1.1
169
+        303 => 'See Other',
170
+        304 => 'Not Modified',
171
+        305 => 'Use Proxy',
172
+        307 => 'Temporary Redirect',
173
+
174
+        // 4xx: Client Error - The request contains bad syntax or cannot be
175
+        // fulfilled
176
+        400 => 'Bad Request',
177
+        401 => 'Unauthorized',
178
+        402 => 'Payment Required',
179
+        403 => 'Forbidden',
180
+        404 => 'Not Found',
181
+        405 => 'Method Not Allowed',
182
+        406 => 'Not Acceptable',
183
+        407 => 'Proxy Authentication Required',
184
+        408 => 'Request Timeout',
185
+        409 => 'Conflict',
186
+        410 => 'Gone',
187
+        411 => 'Length Required',
188
+        412 => 'Precondition Failed',
189
+        413 => 'Request Entity Too Large',
190
+        414 => 'Request-URI Too Long',
191
+        415 => 'Unsupported Media Type',
192
+        416 => 'Requested Range Not Satisfiable',
193
+        417 => 'Expectation Failed',
194
+
195
+        // 5xx: Server Error - The server failed to fulfill an apparently
196
+        // valid request
197
+        500 => 'Internal Server Error',
198
+        501 => 'Not Implemented',
199
+        502 => 'Bad Gateway',
200
+        503 => 'Service Unavailable',
201
+        504 => 'Gateway Timeout',
202
+        505 => 'HTTP Version Not Supported',
203
+        509 => 'Bandwidth Limit Exceeded',
204
+
205
+    );
206
+
207
+   /**
208
+    * Returns the default reason phrase for the given code or all reason phrases
209
+    *
210
+    * @param  int $code         Response code
211
+    * @return string|array|null Default reason phrase for $code if $code is given
212
+    *                           (null if no phrase is available), array of all
213
+    *                           reason phrases if $code is null
214
+    * @link   http://pear.php.net/bugs/18716
215
+    */
216
+    public static function getDefaultReasonPhrase($code = null)
217
+    {
218
+        if (null === $code) {
219
+            return self::$phrases;
220
+        } else {
221
+            return isset(self::$phrases[$code]) ? self::$phrases[$code] : null;
222
+        }
223
+    }
224
+
225
+   /**
226
+    * Constructor, parses the response status line
227
+    *
228
+    * @param    string Response status line (e.g. "HTTP/1.1 200 OK")
229
+    * @param    bool   Whether body is still encoded by Content-Encoding
230
+    * @param    string Effective URL of the response
231
+    * @throws   HTTP_Request2_MessageException if status line is invalid according to spec
232
+    */
233
+    public function __construct($statusLine, $bodyEncoded = true, $effectiveUrl = null)
234
+    {
235
+        if (!preg_match('!^HTTP/(\d\.\d) (\d{3})(?: (.+))?!', $statusLine, $m)) {
236
+            throw new HTTP_Request2_MessageException(
237
+                "Malformed response: {$statusLine}",
238
+                HTTP_Request2_Exception::MALFORMED_RESPONSE
239
+            );
240
+        }
241
+        $this->version      = $m[1];
242
+        $this->code         = intval($m[2]);
243
+        $this->reasonPhrase = !empty($m[3]) ? trim($m[3]) : self::getDefaultReasonPhrase($this->code);
244
+        $this->bodyEncoded  = (bool)$bodyEncoded;
245
+        $this->effectiveUrl = (string)$effectiveUrl;
246
+    }
247
+
248
+   /**
249
+    * Parses the line from HTTP response filling $headers array
250
+    *
251
+    * The method should be called after reading the line from socket or receiving
252
+    * it into cURL callback. Passing an empty string here indicates the end of
253
+    * response headers and triggers additional processing, so be sure to pass an
254
+    * empty string in the end.
255
+    *
256
+    * @param    string  Line from HTTP response
257
+    */
258
+    public function parseHeaderLine($headerLine)
259
+    {
260
+        $headerLine = trim($headerLine, "\r\n");
261
+
262
+        // empty string signals the end of headers, process the received ones
263
+        if ('' == $headerLine) {
264
+            if (!empty($this->headers['set-cookie'])) {
265
+                $cookies = is_array($this->headers['set-cookie'])?
266
+                           $this->headers['set-cookie']:
267
+                           array($this->headers['set-cookie']);
268
+                foreach ($cookies as $cookieString) {
269
+                    $this->parseCookie($cookieString);
270
+                }
271
+                unset($this->headers['set-cookie']);
272
+            }
273
+            foreach (array_keys($this->headers) as $k) {
274
+                if (is_array($this->headers[$k])) {
275
+                    $this->headers[$k] = implode(', ', $this->headers[$k]);
276
+                }
277
+            }
278
+
279
+        // string of the form header-name: header value
280
+        } elseif (preg_match('!^([^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+):(.+)$!', $headerLine, $m)) {
281
+            $name  = strtolower($m[1]);
282
+            $value = trim($m[2]);
283
+            if (empty($this->headers[$name])) {
284
+                $this->headers[$name] = $value;
285
+            } else {
286
+                if (!is_array($this->headers[$name])) {
287
+                    $this->headers[$name] = array($this->headers[$name]);
288
+                }
289
+                $this->headers[$name][] = $value;
290
+            }
291
+            $this->lastHeader = $name;
292
+
293
+        // continuation of a previous header
294
+        } elseif (preg_match('!^\s+(.+)$!', $headerLine, $m) && $this->lastHeader) {
295
+            if (!is_array($this->headers[$this->lastHeader])) {
296
+                $this->headers[$this->lastHeader] .= ' ' . trim($m[1]);
297
+            } else {
298
+                $key = count($this->headers[$this->lastHeader]) - 1;
299
+                $this->headers[$this->lastHeader][$key] .= ' ' . trim($m[1]);
300
+            }
301
+        }
302
+    }
303
+
304
+   /**
305
+    * Parses a Set-Cookie header to fill $cookies array
306
+    *
307
+    * @param    string    value of Set-Cookie header
308
+    * @link     http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html
309
+    */
310
+    protected function parseCookie($cookieString)
311
+    {
312
+        $cookie = array(
313
+            'expires' => null,
314
+            'domain'  => null,
315
+            'path'    => null,
316
+            'secure'  => false
317
+        );
318
+
319
+        // Only a name=value pair
320
+        if (!strpos($cookieString, ';')) {
321
+            $pos = strpos($cookieString, '=');
322
+            $cookie['name']  = trim(substr($cookieString, 0, $pos));
323
+            $cookie['value'] = trim(substr($cookieString, $pos + 1));
324
+
325
+        // Some optional parameters are supplied
326
+        } else {
327
+            $elements = explode(';', $cookieString);
328
+            $pos = strpos($elements[0], '=');
329
+            $cookie['name']  = trim(substr($elements[0], 0, $pos));
330
+            $cookie['value'] = trim(substr($elements[0], $pos + 1));
331
+
332
+            for ($i = 1; $i < count($elements); $i++) {
333
+                if (false === strpos($elements[$i], '=')) {
334
+                    $elName  = trim($elements[$i]);
335
+                    $elValue = null;
336
+                } else {
337
+                    list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
338
+                }
339
+                $elName = strtolower($elName);
340
+                if ('secure' == $elName) {
341
+                    $cookie['secure'] = true;
342
+                } elseif ('expires' == $elName) {
343
+                    $cookie['expires'] = str_replace('"', '', $elValue);
344
+                } elseif ('path' == $elName || 'domain' == $elName) {
345
+                    $cookie[$elName] = urldecode($elValue);
346
+                } else {
347
+                    $cookie[$elName] = $elValue;
348
+                }
349
+            }
350
+        }
351
+        $this->cookies[] = $cookie;
352
+    }
353
+
354
+   /**
355
+    * Appends a string to the response body
356
+    * @param    string
357
+    */
358
+    public function appendBody($bodyChunk)
359
+    {
360
+        $this->body .= $bodyChunk;
361
+    }
362
+
363
+   /**
364
+    * Returns the effective URL of the response
365
+    *
366
+    * This may be different from the request URL if redirects were followed.
367
+    *
368
+    * @return string
369
+    * @link   http://pear.php.net/bugs/bug.php?id=18412
370
+    */
371
+    public function getEffectiveUrl()
372
+    {
373
+        return $this->effectiveUrl;
374
+    }
375
+
376
+   /**
377
+    * Returns the status code
378
+    * @return   integer
379
+    */
380
+    public function getStatus()
381
+    {
382
+        return $this->code;
383
+    }
384
+
385
+   /**
386
+    * Returns the reason phrase
387
+    * @return   string
388
+    */
389
+    public function getReasonPhrase()
390
+    {
391
+        return $this->reasonPhrase;
392
+    }
393
+
394
+   /**
395
+    * Whether response is a redirect that can be automatically handled by HTTP_Request2
396
+    * @return   bool
397
+    */
398
+    public function isRedirect()
399
+    {
400
+        return in_array($this->code, array(300, 301, 302, 303, 307))
401
+               && isset($this->headers['location']);
402
+    }
403
+
404
+   /**
405
+    * Returns either the named header or all response headers
406
+    *
407
+    * @param    string          Name of header to return
408
+    * @return   string|array    Value of $headerName header (null if header is
409
+    *                           not present), array of all response headers if
410
+    *                           $headerName is null
411
+    */
412
+    public function getHeader($headerName = null)
413
+    {
414
+        if (null === $headerName) {
415
+            return $this->headers;
416
+        } else {
417
+            $headerName = strtolower($headerName);
418
+            return isset($this->headers[$headerName])? $this->headers[$headerName]: null;
419
+        }
420
+    }
421
+
422
+   /**
423
+    * Returns cookies set in response
424
+    *
425
+    * @return   array
426
+    */
427
+    public function getCookies()
428
+    {
429
+        return $this->cookies;
430
+    }
431
+
432
+   /**
433
+    * Returns the body of the response
434
+    *
435
+    * @return   string
436
+    * @throws   HTTP_Request2_Exception if body cannot be decoded
437
+    */
438
+    public function getBody()
439
+    {
440
+        if (0 == strlen($this->body) || !$this->bodyEncoded ||
441
+            !in_array(strtolower($this->getHeader('content-encoding')), array('gzip', 'deflate'))
442
+        ) {
443
+            return $this->body;
444
+
445
+        } else {
446
+            if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) {
447
+                $oldEncoding = mb_internal_encoding();
448
+                mb_internal_encoding('iso-8859-1');
449
+            }
450
+
451
+            try {
452
+                switch (strtolower($this->getHeader('content-encoding'))) {
453
+                    case 'gzip':
454
+                        $decoded = self::decodeGzip($this->body);
455
+                        break;
456
+                    case 'deflate':
457
+                        $decoded = self::decodeDeflate($this->body);
458
+                }
459
+            } catch (Exception $e) {
460
+            }
461
+
462
+            if (!empty($oldEncoding)) {
463
+                mb_internal_encoding($oldEncoding);
464
+            }
465
+            if (!empty($e)) {
466
+                throw $e;
467
+            }
468
+            return $decoded;
469
+        }
470
+    }
471
+
472
+   /**
473
+    * Get the HTTP version of the response
474
+    *
475
+    * @return   string
476
+    */
477
+    public function getVersion()
478
+    {
479
+        return $this->version;
480
+    }
481
+
482
+   /**
483
+    * Decodes the message-body encoded by gzip
484
+    *
485
+    * The real decoding work is done by gzinflate() built-in function, this
486
+    * method only parses the header and checks data for compliance with
487
+    * RFC 1952
488
+    *
489
+    * @param    string  gzip-encoded data
490
+    * @return   string  decoded data
491
+    * @throws   HTTP_Request2_LogicException
492
+    * @throws   HTTP_Request2_MessageException
493
+    * @link     http://tools.ietf.org/html/rfc1952
494
+    */
495
+    public static function decodeGzip($data)
496
+    {
497
+        $length = strlen($data);
498
+        // If it doesn't look like gzip-encoded data, don't bother
499
+        if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {
500
+            return $data;
501
+        }
502
+        if (!function_exists('gzinflate')) {
503
+            throw new HTTP_Request2_LogicException(
504
+                'Unable to decode body: gzip extension not available',
505
+                HTTP_Request2_Exception::MISCONFIGURATION
506
+            );
507
+        }
508
+        $method = ord(substr($data, 2, 1));
509
+        if (8 != $method) {
510
+            throw new HTTP_Request2_MessageException(
511
+                'Error parsing gzip header: unknown compression method',
512
+                HTTP_Request2_Exception::DECODE_ERROR
513
+            );
514
+        }
515
+        $flags = ord(substr($data, 3, 1));
516
+        if ($flags & 224) {
517
+            throw new HTTP_Request2_MessageException(
518
+                'Error parsing gzip header: reserved bits are set',
519
+                HTTP_Request2_Exception::DECODE_ERROR
520
+            );
521
+        }
522
+
523
+        // header is 10 bytes minimum. may be longer, though.
524
+        $headerLength = 10;
525
+        // extra fields, need to skip 'em
526
+        if ($flags & 4) {
527
+            if ($length - $headerLength - 2 < 8) {
528
+                throw new HTTP_Request2_MessageException(
529
+                    'Error parsing gzip header: data too short',
530
+                    HTTP_Request2_Exception::DECODE_ERROR
531
+                );
532
+            }
533
+            $extraLength = unpack('v', substr($data, 10, 2));
534
+            if ($length - $headerLength - 2 - $extraLength[1] < 8) {
535
+                throw new HTTP_Request2_MessageException(
536
+                    'Error parsing gzip header: data too short',
537
+                    HTTP_Request2_Exception::DECODE_ERROR
538
+                );
539
+            }
540
+            $headerLength += $extraLength[1] + 2;
541
+        }
542
+        // file name, need to skip that
543
+        if ($flags & 8) {
544
+            if ($length - $headerLength - 1 < 8) {
545
+                throw new HTTP_Request2_MessageException(
546
+                    'Error parsing gzip header: data too short',
547
+                    HTTP_Request2_Exception::DECODE_ERROR
548
+                );
549
+            }
550
+            $filenameLength = strpos(substr($data, $headerLength), chr(0));
551
+            if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {
552
+                throw new HTTP_Request2_MessageException(
553
+                    'Error parsing gzip header: data too short',
554
+                    HTTP_Request2_Exception::DECODE_ERROR
555
+                );
556
+            }
557
+            $headerLength += $filenameLength + 1;
558
+        }
559
+        // comment, need to skip that also
560
+        if ($flags & 16) {
561
+            if ($length - $headerLength - 1 < 8) {
562
+                throw new HTTP_Request2_MessageException(
563
+                    'Error parsing gzip header: data too short',
564
+                    HTTP_Request2_Exception::DECODE_ERROR
565
+                );
566
+            }
567
+            $commentLength = strpos(substr($data, $headerLength), chr(0));
568
+            if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {
569
+                throw new HTTP_Request2_MessageException(
570
+                    'Error parsing gzip header: data too short',
571
+                    HTTP_Request2_Exception::DECODE_ERROR
572
+                );
573
+            }
574
+            $headerLength += $commentLength + 1;
575
+        }
576
+        // have a CRC for header. let's check
577
+        if ($flags & 2) {
578
+            if ($length - $headerLength - 2 < 8) {
579
+                throw new HTTP_Request2_MessageException(
580
+                    'Error parsing gzip header: data too short',
581
+                    HTTP_Request2_Exception::DECODE_ERROR
582
+                );
583
+            }
584
+            $crcReal   = 0xffff & crc32(substr($data, 0, $headerLength));
585
+            $crcStored = unpack('v', substr($data, $headerLength, 2));
586
+            if ($crcReal != $crcStored[1]) {
587
+                throw new HTTP_Request2_MessageException(
588
+                    'Header CRC check failed',
589
+                    HTTP_Request2_Exception::DECODE_ERROR
590
+                );
591
+            }
592
+            $headerLength += 2;
593
+        }
594
+        // unpacked data CRC and size at the end of encoded data
595
+        $tmp = unpack('V2', substr($data, -8));
596
+        $dataCrc  = $tmp[1];
597
+        $dataSize = $tmp[2];
598
+
599
+        // finally, call the gzinflate() function
600
+        // don't pass $dataSize to gzinflate, see bugs #13135, #14370
601
+        $unpacked = gzinflate(substr($data, $headerLength, -8));
602
+        if (false === $unpacked) {
603
+            throw new HTTP_Request2_MessageException(
604
+                'gzinflate() call failed',
605
+                HTTP_Request2_Exception::DECODE_ERROR
606
+            );
607
+        } elseif ($dataSize != strlen($unpacked)) {
608
+            throw new HTTP_Request2_MessageException(
609
+                'Data size check failed',
610
+                HTTP_Request2_Exception::DECODE_ERROR
611
+            );
612
+        } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) {
613
+            throw new HTTP_Request2_Exception(
614
+                'Data CRC check failed',
615
+                HTTP_Request2_Exception::DECODE_ERROR
616
+            );
617
+        }
618
+        return $unpacked;
619
+    }
620
+
621
+   /**
622
+    * Decodes the message-body encoded by deflate
623
+    *
624
+    * @param    string  deflate-encoded data
625
+    * @return   string  decoded data
626
+    * @throws   HTTP_Request2_LogicException
627
+    */
628
+    public static function decodeDeflate($data)
629
+    {
630
+        if (!function_exists('gzuncompress')) {
631
+            throw new HTTP_Request2_LogicException(
632
+                'Unable to decode body: gzip extension not available',
633
+                HTTP_Request2_Exception::MISCONFIGURATION
634
+            );
635
+        }
636
+        // RFC 2616 defines 'deflate' encoding as zlib format from RFC 1950,
637
+        // while many applications send raw deflate stream from RFC 1951.
638
+        // We should check for presence of zlib header and use gzuncompress() or
639
+        // gzinflate() as needed. See bug #15305
640
+        $header = unpack('n', substr($data, 0, 2));
641
+        return (0 == $header[1] % 31)? gzuncompress($data): gzinflate($data);
642
+    }
643
+}
644
+?>
645
\ No newline at end of file
646
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Mail Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Mail/mime.php Added
1478
 
1
@@ -0,0 +1,1476 @@
2
+<?php
3
+/**
4
+ * The Mail_Mime class is used to create MIME E-mail messages
5
+ *
6
+ * The Mail_Mime class provides an OO interface to create MIME
7
+ * enabled email messages. This way you can create emails that
8
+ * contain plain-text bodies, HTML bodies, attachments, inline
9
+ * images and specific headers.
10
+ *
11
+ * Compatible with PHP versions 4 and 5
12
+ *
13
+ * LICENSE: This LICENSE is in the BSD license style.
14
+ * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org>
15
+ * Copyright (c) 2003-2006, PEAR <pear-group@php.net>
16
+ * All rights reserved.
17
+ *
18
+ * Redistribution and use in source and binary forms, with or
19
+ * without modification, are permitted provided that the following
20
+ * conditions are met:
21
+ *
22
+ * - Redistributions of source code must retain the above copyright
23
+ *   notice, this list of conditions and the following disclaimer.
24
+ * - Redistributions in binary form must reproduce the above copyright
25
+ *   notice, this list of conditions and the following disclaimer in the
26
+ *   documentation and/or other materials provided with the distribution.
27
+ * - Neither the name of the authors, nor the names of its contributors 
28
+ *   may be used to endorse or promote products derived from this 
29
+ *   software without specific prior written permission.
30
+ *
31
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
32
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
33
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
35
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
36
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
37
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
38
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
39
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
41
+ * THE POSSIBILITY OF SUCH DAMAGE.
42
+ *
43
+ * @category  Mail
44
+ * @package   Mail_Mime
45
+ * @author    Richard Heyes  <richard@phpguru.org>
46
+ * @author    Tomas V.V. Cox <cox@idecnet.com>
47
+ * @author    Cipriano Groenendal <cipri@php.net>
48
+ * @author    Sean Coates <sean@php.net>
49
+ * @author    Aleksander Machniak <alec@php.net>
50
+ * @copyright 2003-2006 PEAR <pear-group@php.net>
51
+ * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
52
+ * @version   1.8.5
53
+ * @link      http://pear.php.net/package/Mail_mime
54
+ *
55
+ *            This class is based on HTML Mime Mail class from
56
+ *            Richard Heyes <richard@phpguru.org> which was based also
57
+ *            in the mime_mail.class by Tobias Ratschiller <tobias@dnet.it>
58
+ *            and Sascha Schumann <sascha@schumann.cx>
59
+ */
60
+
61
+
62
+/**
63
+ * require PEAR
64
+ *
65
+ * This package depends on PEAR to raise errors.
66
+ */
67
+require_once 'PEAR.php';
68
+
69
+/**
70
+ * require Mail_mimePart
71
+ *
72
+ * Mail_mimePart contains the code required to
73
+ * create all the different parts a mail can
74
+ * consist of.
75
+ */
76
+require_once 'Mail/mimePart.php';
77
+
78
+
79
+/**
80
+ * The Mail_Mime class provides an OO interface to create MIME
81
+ * enabled email messages. This way you can create emails that
82
+ * contain plain-text bodies, HTML bodies, attachments, inline
83
+ * images and specific headers.
84
+ *
85
+ * @category  Mail
86
+ * @package   Mail_Mime
87
+ * @author    Richard Heyes  <richard@phpguru.org>
88
+ * @author    Tomas V.V. Cox <cox@idecnet.com>
89
+ * @author    Cipriano Groenendal <cipri@php.net>
90
+ * @author    Sean Coates <sean@php.net>
91
+ * @copyright 2003-2006 PEAR <pear-group@php.net>
92
+ * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
93
+ * @version   Release: 1.8.5
94
+ * @link      http://pear.php.net/package/Mail_mime
95
+ */
96
+class Mail_mime
97
+{
98
+    /**
99
+     * Contains the plain text part of the email
100
+     *
101
+     * @var string
102
+     * @access private
103
+     */
104
+    var $_txtbody;
105
+
106
+    /**
107
+     * Contains the html part of the email
108
+     *
109
+     * @var string
110
+     * @access private
111
+     */
112
+    var $_htmlbody;
113
+
114
+    /**
115
+     * list of the attached images
116
+     *
117
+     * @var array
118
+     * @access private
119
+     */
120
+    var $_html_images = array();
121
+
122
+    /**
123
+     * list of the attachements
124
+     *
125
+     * @var array
126
+     * @access private
127
+     */
128
+    var $_parts = array();
129
+
130
+    /**
131
+     * Headers for the mail
132
+     *
133
+     * @var array
134
+     * @access private
135
+     */
136
+    var $_headers = array();
137
+
138
+    /**
139
+     * Build parameters
140
+     *
141
+     * @var array
142
+     * @access private
143
+     */
144
+    var $_build_params = array(
145
+        // What encoding to use for the headers
146
+        // Options: quoted-printable or base64
147
+        'head_encoding' => 'quoted-printable',
148
+        // What encoding to use for plain text
149
+        // Options: 7bit, 8bit, base64, or quoted-printable
150
+        'text_encoding' => 'quoted-printable',
151
+        // What encoding to use for html
152
+        // Options: 7bit, 8bit, base64, or quoted-printable
153
+        'html_encoding' => 'quoted-printable',
154
+        // The character set to use for html
155
+        'html_charset'  => 'ISO-8859-1',
156
+        // The character set to use for text
157
+        'text_charset'  => 'ISO-8859-1',
158
+        // The character set to use for headers
159
+        'head_charset'  => 'ISO-8859-1',
160
+        // End-of-line sequence
161
+        'eol'           => "\r\n",
162
+        // Delay attachment files IO until building the message
163
+        'delay_file_io' => false
164
+    );
165
+
166
+    /**
167
+     * Constructor function
168
+     *
169
+     * @param mixed $params Build parameters that change the way the email
170
+     *                      is built. Should be an associative array.
171
+     *                      See $_build_params.
172
+     *
173
+     * @return void
174
+     * @access public
175
+     */
176
+    function Mail_mime($params = array())
177
+    {
178
+        // Backward-compatible EOL setting
179
+        if (is_string($params)) {
180
+            $this->_build_params['eol'] = $params;
181
+        } else if (defined('MAIL_MIME_CRLF') && !isset($params['eol'])) {
182
+            $this->_build_params['eol'] = MAIL_MIME_CRLF;
183
+        }
184
+
185
+        // Update build parameters
186
+        if (!empty($params) && is_array($params)) {
187
+            while (list($key, $value) = each($params)) {
188
+                $this->_build_params[$key] = $value;
189
+            }
190
+        }
191
+    }
192
+
193
+    /**
194
+     * Set build parameter value
195
+     *
196
+     * @param string $name  Parameter name
197
+     * @param string $value Parameter value
198
+     *
199
+     * @return void
200
+     * @access public
201
+     * @since 1.6.0
202
+     */
203
+    function setParam($name, $value)
204
+    {
205
+        $this->_build_params[$name] = $value;
206
+    }
207
+
208
+    /**
209
+     * Get build parameter value
210
+     *
211
+     * @param string $name Parameter name
212
+     *
213
+     * @return mixed Parameter value
214
+     * @access public
215
+     * @since 1.6.0
216
+     */
217
+    function getParam($name)
218
+    {
219
+        return isset($this->_build_params[$name]) ? $this->_build_params[$name] : null;
220
+    }
221
+
222
+    /**
223
+     * Accessor function to set the body text. Body text is used if
224
+     * it's not an html mail being sent or else is used to fill the
225
+     * text/plain part that emails clients who don't support
226
+     * html should show.
227
+     *
228
+     * @param string $data   Either a string or
229
+     *                       the file name with the contents
230
+     * @param bool   $isfile If true the first param should be treated
231
+     *                       as a file name, else as a string (default)
232
+     * @param bool   $append If true the text or file is appended to
233
+     *                       the existing body, else the old body is
234
+     *                       overwritten
235
+     *
236
+     * @return mixed         True on success or PEAR_Error object
237
+     * @access public
238
+     */
239
+    function setTXTBody($data, $isfile = false, $append = false)
240
+    {
241
+        if (!$isfile) {
242
+            if (!$append) {
243
+                $this->_txtbody = $data;
244
+            } else {
245
+                $this->_txtbody .= $data;
246
+            }
247
+        } else {
248
+            $cont = $this->_file2str($data);
249
+            if (PEAR::isError($cont)) {
250
+                return $cont;
251
+            }
252
+            if (!$append) {
253
+                $this->_txtbody = $cont;
254
+            } else {
255
+                $this->_txtbody .= $cont;
256
+            }
257
+        }
258
+        return true;
259
+    }
260
+
261
+    /**
262
+     * Get message text body
263
+     *
264
+     * @return string Text body
265
+     * @access public
266
+     * @since 1.6.0
267
+     */
268
+    function getTXTBody()
269
+    {
270
+        return $this->_txtbody;
271
+    }
272
+
273
+    /**
274
+     * Adds a html part to the mail.
275
+     *
276
+     * @param string $data   Either a string or the file name with the
277
+     *                       contents
278
+     * @param bool   $isfile A flag that determines whether $data is a
279
+     *                       filename, or a string(false, default)
280
+     *
281
+     * @return bool          True on success
282
+     * @access public
283
+     */
284
+    function setHTMLBody($data, $isfile = false)
285
+    {
286
+        if (!$isfile) {
287
+            $this->_htmlbody = $data;
288
+        } else {
289
+            $cont = $this->_file2str($data);
290
+            if (PEAR::isError($cont)) {
291
+                return $cont;
292
+            }
293
+            $this->_htmlbody = $cont;
294
+        }
295
+
296
+        return true;
297
+    }
298
+
299
+    /**
300
+     * Get message HTML body
301
+     *
302
+     * @return string HTML body
303
+     * @access public
304
+     * @since 1.6.0
305
+     */
306
+    function getHTMLBody()
307
+    {
308
+        return $this->_htmlbody;
309
+    }
310
+
311
+    /**
312
+     * Adds an image to the list of embedded images.
313
+     *
314
+     * @param string $file       The image file name OR image data itself
315
+     * @param string $c_type     The content type
316
+     * @param string $name       The filename of the image.
317
+     *                           Only used if $file is the image data.
318
+     * @param bool   $isfile     Whether $file is a filename or not.
319
+     *                           Defaults to true
320
+     * @param string $content_id Desired Content-ID of MIME part
321
+     *                           Defaults to generated unique ID
322
+     *
323
+     * @return bool          True on success
324
+     * @access public
325
+     */
326
+    function addHTMLImage($file,
327
+        $c_type='application/octet-stream',
328
+        $name = '',
329
+        $isfile = true,
330
+        $content_id = null
331
+    ) {
332
+        $bodyfile = null;
333
+
334
+        if ($isfile) {
335
+            // Don't load file into memory
336
+            if ($this->_build_params['delay_file_io']) {
337
+                $filedata = null;
338
+                $bodyfile = $file;
339
+            } else {
340
+                if (PEAR::isError($filedata = $this->_file2str($file))) {
341
+                    return $filedata;
342
+                }
343
+            }
344
+            $filename = ($name ? $name : $file);
345
+        } else {
346
+            $filedata = $file;
347
+            $filename = $name;
348
+        }
349
+
350
+        if (!$content_id) {
351
+            $content_id = md5(uniqid(time()));
352
+        }
353
+
354
+        $this->_html_images[] = array(
355
+            'body'      => $filedata,
356
+            'body_file' => $bodyfile,
357
+            'name'      => $filename,
358
+            'c_type'    => $c_type,
359
+            'cid'       => $content_id
360
+        );
361
+
362
+        return true;
363
+    }
364
+
365
+    /**
366
+     * Adds a file to the list of attachments.
367
+     *
368
+     * @param string $file        The file name of the file to attach
369
+     *                            or the file contents itself
370
+     * @param string $c_type      The content type
371
+     * @param string $name        The filename of the attachment
372
+     *                            Only use if $file is the contents
373
+     * @param bool   $isfile      Whether $file is a filename or not. Defaults to true
374
+     * @param string $encoding    The type of encoding to use. Defaults to base64.
375
+     *                            Possible values: 7bit, 8bit, base64 or quoted-printable.
376
+     * @param string $disposition The content-disposition of this file
377
+     *                            Defaults to attachment.
378
+     *                            Possible values: attachment, inline.
379
+     * @param string $charset     The character set of attachment's content.
380
+     * @param string $language    The language of the attachment
381
+     * @param string $location    The RFC 2557.4 location of the attachment
382
+     * @param string $n_encoding  Encoding of the attachment's name in Content-Type
383
+     *                            By default filenames are encoded using RFC2231 method
384
+     *                            Here you can set RFC2047 encoding (quoted-printable
385
+     *                            or base64) instead
386
+     * @param string $f_encoding  Encoding of the attachment's filename
387
+     *                            in Content-Disposition header.
388
+     * @param string $description Content-Description header
389
+     * @param string $h_charset   The character set of the headers e.g. filename
390
+     *                            If not specified, $charset will be used
391
+     * @param array  $add_headers Additional part headers. Array keys can be in form
392
+     *                            of <header_name>:<parameter_name>
393
+     *
394
+     * @return mixed              True on success or PEAR_Error object
395
+     * @access public
396
+     */
397
+    function addAttachment($file,
398
+        $c_type      = 'application/octet-stream',
399
+        $name        = '',
400
+        $isfile      = true,
401
+        $encoding    = 'base64',
402
+        $disposition = 'attachment',
403
+        $charset     = '',
404
+        $language    = '',
405
+        $location    = '',
406
+        $n_encoding  = null,
407
+        $f_encoding  = null,
408
+        $description = '',
409
+        $h_charset   = null,
410
+        $add_headers = array()
411
+    ) {
412
+        $bodyfile = null;
413
+
414
+        if ($isfile) {
415
+            // Don't load file into memory
416
+            if ($this->_build_params['delay_file_io']) {
417
+                $filedata = null;
418
+                $bodyfile = $file;
419
+            } else {
420
+                if (PEAR::isError($filedata = $this->_file2str($file))) {
421
+                    return $filedata;
422
+                }
423
+            }
424
+            // Force the name the user supplied, otherwise use $file
425
+            $filename = ($name ? $name : $file);
426
+        } else {
427
+            $filedata = $file;
428
+            $filename = $name;
429
+        }
430
+
431
+        if (!strlen($filename)) {
432
+            $msg = "The supplied filename for the attachment can't be empty";
433
+            $err = PEAR::raiseError($msg);
434
+            return $err;
435
+        }
436
+        $filename = $this->_basename($filename);
437
+
438
+        $this->_parts[] = array(
439
+            'body'        => $filedata,
440
+            'body_file'   => $bodyfile,
441
+            'name'        => $filename,
442
+            'c_type'      => $c_type,
443
+            'charset'     => $charset,
444
+            'encoding'    => $encoding,
445
+            'language'    => $language,
446
+            'location'    => $location,
447
+            'disposition' => $disposition,
448
+            'description' => $description,
449
+            'add_headers' => $add_headers,
450
+            'name_encoding'     => $n_encoding,
451
+            'filename_encoding' => $f_encoding,
452
+            'headers_charset'   => $h_charset,
453
+        );
454
+
455
+        return true;
456
+    }
457
+
458
+    /**
459
+     * Get the contents of the given file name as string
460
+     *
461
+     * @param string $file_name Path of file to process
462
+     *
463
+     * @return string           Contents of $file_name
464
+     * @access private
465
+     */
466
+    function &_file2str($file_name)
467
+    {
468
+        // Check state of file and raise an error properly
469
+        if (!file_exists($file_name)) {
470
+            $err = PEAR::raiseError('File not found: ' . $file_name);
471
+            return $err;
472
+        }
473
+        if (!is_file($file_name)) {
474
+            $err = PEAR::raiseError('Not a regular file: ' . $file_name);
475
+            return $err;
476
+        }
477
+        if (!is_readable($file_name)) {
478
+            $err = PEAR::raiseError('File is not readable: ' . $file_name);
479
+            return $err;
480
+        }
481
+
482
+        // Temporarily reset magic_quotes_runtime and read file contents
483
+        if ($magic_quote_setting = get_magic_quotes_runtime()) {
484
+            @ini_set('magic_quotes_runtime', 0);
485
+        }
486
+        $cont = file_get_contents($file_name);
487
+        if ($magic_quote_setting) {
488
+            @ini_set('magic_quotes_runtime', $magic_quote_setting);
489
+        }
490
+
491
+        return $cont;
492
+    }
493
+
494
+    /**
495
+     * Adds a text subpart to the mimePart object and
496
+     * returns it during the build process.
497
+     *
498
+     * @param mixed  &$obj The object to add the part to, or
499
+     *                     null if a new object is to be created.
500
+     * @param string $text The text to add.
501
+     *
502
+     * @return object      The text mimePart object
503
+     * @access private
504
+     */
505
+    function &_addTextPart(&$obj, $text)
506
+    {
507
+        $params['content_type'] = 'text/plain';
508
+        $params['encoding']     = $this->_build_params['text_encoding'];
509
+        $params['charset']      = $this->_build_params['text_charset'];
510
+        $params['eol']          = $this->_build_params['eol'];
511
+
512
+        if (is_object($obj)) {
513
+            $ret = $obj->addSubpart($text, $params);
514
+            return $ret;
515
+        } else {
516
+            $ret = new Mail_mimePart($text, $params);
517
+            return $ret;
518
+        }
519
+    }
520
+
521
+    /**
522
+     * Adds a html subpart to the mimePart object and
523
+     * returns it during the build process.
524
+     *
525
+     * @param mixed &$obj The object to add the part to, or
526
+     *                    null if a new object is to be created.
527
+     *
528
+     * @return object     The html mimePart object
529
+     * @access private
530
+     */
531
+    function &_addHtmlPart(&$obj)
532
+    {
533
+        $params['content_type'] = 'text/html';
534
+        $params['encoding']     = $this->_build_params['html_encoding'];
535
+        $params['charset']      = $this->_build_params['html_charset'];
536
+        $params['eol']          = $this->_build_params['eol'];
537
+
538
+        if (is_object($obj)) {
539
+            $ret = $obj->addSubpart($this->_htmlbody, $params);
540
+            return $ret;
541
+        } else {
542
+            $ret = new Mail_mimePart($this->_htmlbody, $params);
543
+            return $ret;
544
+        }
545
+    }
546
+
547
+    /**
548
+     * Creates a new mimePart object, using multipart/mixed as
549
+     * the initial content-type and returns it during the
550
+     * build process.
551
+     *
552
+     * @return object The multipart/mixed mimePart object
553
+     * @access private
554
+     */
555
+    function &_addMixedPart()
556
+    {
557
+        $params                 = array();
558
+        $params['content_type'] = 'multipart/mixed';
559
+        $params['eol']          = $this->_build_params['eol'];
560
+
561
+        // Create empty multipart/mixed Mail_mimePart object to return
562
+        $ret = new Mail_mimePart('', $params);
563
+        return $ret;
564
+    }
565
+
566
+    /**
567
+     * Adds a multipart/alternative part to a mimePart
568
+     * object (or creates one), and returns it during
569
+     * the build process.
570
+     *
571
+     * @param mixed &$obj The object to add the part to, or
572
+     *                    null if a new object is to be created.
573
+     *
574
+     * @return object     The multipart/mixed mimePart object
575
+     * @access private
576
+     */
577
+    function &_addAlternativePart(&$obj)
578
+    {
579
+        $params['content_type'] = 'multipart/alternative';
580
+        $params['eol']          = $this->_build_params['eol'];
581
+
582
+        if (is_object($obj)) {
583
+            return $obj->addSubpart('', $params);
584
+        } else {
585
+            $ret = new Mail_mimePart('', $params);
586
+            return $ret;
587
+        }
588
+    }
589
+
590
+    /**
591
+     * Adds a multipart/related part to a mimePart
592
+     * object (or creates one), and returns it during
593
+     * the build process.
594
+     *
595
+     * @param mixed &$obj The object to add the part to, or
596
+     *                    null if a new object is to be created
597
+     *
598
+     * @return object     The multipart/mixed mimePart object
599
+     * @access private
600
+     */
601
+    function &_addRelatedPart(&$obj)
602
+    {
603
+        $params['content_type'] = 'multipart/related';
604
+        $params['eol']          = $this->_build_params['eol'];
605
+
606
+        if (is_object($obj)) {
607
+            return $obj->addSubpart('', $params);
608
+        } else {
609
+            $ret = new Mail_mimePart('', $params);
610
+            return $ret;
611
+        }
612
+    }
613
+
614
+    /**
615
+     * Adds an html image subpart to a mimePart object
616
+     * and returns it during the build process.
617
+     *
618
+     * @param object &$obj  The mimePart to add the image to
619
+     * @param array  $value The image information
620
+     *
621
+     * @return object       The image mimePart object
622
+     * @access private
623
+     */
624
+    function &_addHtmlImagePart(&$obj, $value)
625
+    {
626
+        $params['content_type'] = $value['c_type'];
627
+        $params['encoding']     = 'base64';
628
+        $params['disposition']  = 'inline';
629
+        $params['filename']     = $value['name'];
630
+        $params['cid']          = $value['cid'];
631
+        $params['body_file']    = $value['body_file'];
632
+        $params['eol']          = $this->_build_params['eol'];
633
+
634
+        if (!empty($value['name_encoding'])) {
635
+            $params['name_encoding'] = $value['name_encoding'];
636
+        }
637
+        if (!empty($value['filename_encoding'])) {
638
+            $params['filename_encoding'] = $value['filename_encoding'];
639
+        }
640
+
641
+        $ret = $obj->addSubpart($value['body'], $params);
642
+        return $ret;
643
+    }
644
+
645
+    /**
646
+     * Adds an attachment subpart to a mimePart object
647
+     * and returns it during the build process.
648
+     *
649
+     * @param object &$obj  The mimePart to add the image to
650
+     * @param array  $value The attachment information
651
+     *
652
+     * @return object       The image mimePart object
653
+     * @access private
654
+     */
655
+    function &_addAttachmentPart(&$obj, $value)
656
+    {
657
+        $params['eol']          = $this->_build_params['eol'];
658
+        $params['filename']     = $value['name'];
659
+        $params['encoding']     = $value['encoding'];
660
+        $params['content_type'] = $value['c_type'];
661
+        $params['body_file']    = $value['body_file'];
662
+        $params['disposition']  = isset($value['disposition']) ? 
663
+                                  $value['disposition'] : 'attachment';
664
+
665
+        // content charset
666
+        if (!empty($value['charset'])) {
667
+            $params['charset'] = $value['charset'];
668
+        }
669
+        // headers charset (filename, description)
670
+        if (!empty($value['headers_charset'])) {
671
+            $params['headers_charset'] = $value['headers_charset'];
672
+        }
673
+        if (!empty($value['language'])) {
674
+            $params['language'] = $value['language'];
675
+        }
676
+        if (!empty($value['location'])) {
677
+            $params['location'] = $value['location'];
678
+        }
679
+        if (!empty($value['name_encoding'])) {
680
+            $params['name_encoding'] = $value['name_encoding'];
681
+        }
682
+        if (!empty($value['filename_encoding'])) {
683
+            $params['filename_encoding'] = $value['filename_encoding'];
684
+        }
685
+        if (!empty($value['description'])) {
686
+            $params['description'] = $value['description'];
687
+        }
688
+        if (is_array($value['add_headers'])) {
689
+            $params['headers'] = $value['add_headers'];
690
+        }
691
+
692
+        $ret = $obj->addSubpart($value['body'], $params);
693
+        return $ret;
694
+    }
695
+
696
+    /**
697
+     * Returns the complete e-mail, ready to send using an alternative
698
+     * mail delivery method. Note that only the mailpart that is made
699
+     * with Mail_Mime is created. This means that,
700
+     * YOU WILL HAVE NO TO: HEADERS UNLESS YOU SET IT YOURSELF 
701
+     * using the $headers parameter!
702
+     * 
703
+     * @param string $separation The separation between these two parts.
704
+     * @param array  $params     The Build parameters passed to the
705
+     *                           &get() function. See &get for more info.
706
+     * @param array  $headers    The extra headers that should be passed
707
+     *                           to the &headers() function.
708
+     *                           See that function for more info.
709
+     * @param bool   $overwrite  Overwrite the existing headers with new.
710
+     *
711
+     * @return mixed The complete e-mail or PEAR error object
712
+     * @access public
713
+     */
714
+    function getMessage($separation = null, $params = null, $headers = null,
715
+        $overwrite = false
716
+    ) {
717
+        if ($separation === null) {
718
+            $separation = $this->_build_params['eol'];
719
+        }
720
+
721
+        $body = $this->get($params);
722
+
723
+        if (PEAR::isError($body)) {
724
+            return $body;
725
+        }
726
+
727
+        $head = $this->txtHeaders($headers, $overwrite);
728
+        $mail = $head . $separation . $body;
729
+        return $mail;
730
+    }
731
+
732
+    /**
733
+     * Returns the complete e-mail body, ready to send using an alternative
734
+     * mail delivery method.
735
+     * 
736
+     * @param array $params The Build parameters passed to the
737
+     *                      &get() function. See &get for more info.
738
+     *
739
+     * @return mixed The e-mail body or PEAR error object
740
+     * @access public
741
+     * @since 1.6.0
742
+     */
743
+    function getMessageBody($params = null)
744
+    {
745
+        return $this->get($params, null, true);
746
+    }
747
+
748
+    /**
749
+     * Writes (appends) the complete e-mail into file.
750
+     * 
751
+     * @param string $filename  Output file location
752
+     * @param array  $params    The Build parameters passed to the
753
+     *                          &get() function. See &get for more info.
754
+     * @param array  $headers   The extra headers that should be passed
755
+     *                          to the &headers() function.
756
+     *                          See that function for more info.
757
+     * @param bool   $overwrite Overwrite the existing headers with new.
758
+     *
759
+     * @return mixed True or PEAR error object
760
+     * @access public
761
+     * @since 1.6.0
762
+     */
763
+    function saveMessage($filename, $params = null, $headers = null, $overwrite = false)
764
+    {
765
+        // Check state of file and raise an error properly
766
+        if (file_exists($filename) && !is_writable($filename)) {
767
+            $err = PEAR::raiseError('File is not writable: ' . $filename);
768
+            return $err;
769
+        }
770
+
771
+        // Temporarily reset magic_quotes_runtime and read file contents
772
+        if ($magic_quote_setting = get_magic_quotes_runtime()) {
773
+            @ini_set('magic_quotes_runtime', 0);
774
+        }
775
+
776
+        if (!($fh = fopen($filename, 'ab'))) {
777
+            $err = PEAR::raiseError('Unable to open file: ' . $filename);
778
+            return $err;
779
+        }
780
+
781
+        // Write message headers into file (skipping Content-* headers)
782
+        $head = $this->txtHeaders($headers, $overwrite, true);
783
+        if (fwrite($fh, $head) === false) {
784
+            $err = PEAR::raiseError('Error writing to file: ' . $filename);
785
+            return $err;
786
+        }
787
+
788
+        fclose($fh);
789
+
790
+        if ($magic_quote_setting) {
791
+            @ini_set('magic_quotes_runtime', $magic_quote_setting);
792
+        }
793
+
794
+        // Write the rest of the message into file
795
+        $res = $this->get($params, $filename);
796
+
797
+        return $res ? $res : true;
798
+    }
799
+
800
+    /**
801
+     * Writes (appends) the complete e-mail body into file.
802
+     * 
803
+     * @param string $filename Output file location
804
+     * @param array  $params   The Build parameters passed to the
805
+     *                         &get() function. See &get for more info.
806
+     *
807
+     * @return mixed True or PEAR error object
808
+     * @access public
809
+     * @since 1.6.0
810
+     */
811
+    function saveMessageBody($filename, $params = null)
812
+    {
813
+        // Check state of file and raise an error properly
814
+        if (file_exists($filename) && !is_writable($filename)) {
815
+            $err = PEAR::raiseError('File is not writable: ' . $filename);
816
+            return $err;
817
+        }
818
+
819
+        // Temporarily reset magic_quotes_runtime and read file contents
820
+        if ($magic_quote_setting = get_magic_quotes_runtime()) {
821
+            @ini_set('magic_quotes_runtime', 0);
822
+        }
823
+
824
+        if (!($fh = fopen($filename, 'ab'))) {
825
+            $err = PEAR::raiseError('Unable to open file: ' . $filename);
826
+            return $err;
827
+        }
828
+
829
+        // Write the rest of the message into file
830
+        $res = $this->get($params, $filename, true);
831
+
832
+        return $res ? $res : true;
833
+    }
834
+
835
+    /**
836
+     * Builds the multipart message from the list ($this->_parts) and
837
+     * returns the mime content.
838
+     *
839
+     * @param array    $params    Build parameters that change the way the email
840
+     *                            is built. Should be associative. See $_build_params.
841
+     * @param resource $filename  Output file where to save the message instead of
842
+     *                            returning it
843
+     * @param boolean  $skip_head True if you want to return/save only the message
844
+     *                            without headers
845
+     *
846
+     * @return mixed The MIME message content string, null or PEAR error object
847
+     * @access public
848
+     */
849
+    function &get($params = null, $filename = null, $skip_head = false)
850
+    {
851
+        if (isset($params)) {
852
+            while (list($key, $value) = each($params)) {
853
+                $this->_build_params[$key] = $value;
854
+            }
855
+        }
856
+
857
+        if (isset($this->_headers['From'])) {
858
+            // Bug #11381: Illegal characters in domain ID
859
+            if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', $this->_headers['From'], $matches)) {
860
+                $domainID = $matches[1];
861
+            } else {
862
+                $domainID = '@localhost';
863
+            }
864
+            foreach ($this->_html_images as $i => $img) {
865
+                $cid = $this->_html_images[$i]['cid']; 
866
+                if (!preg_match('#'.preg_quote($domainID).'$#', $cid)) {
867
+                    $this->_html_images[$i]['cid'] = $cid . $domainID;
868
+                }
869
+            }
870
+        }
871
+
872
+        if (count($this->_html_images) && isset($this->_htmlbody)) {
873
+            foreach ($this->_html_images as $key => $value) {
874
+                $regex   = array();
875
+                $regex[] = '#(\s)((?i)src|background|href(?-i))\s*=\s*(["\']?)' .
876
+                            preg_quote($value['name'], '#') . '\3#';
877
+                $regex[] = '#(?i)url(?-i)\(\s*(["\']?)' .
878
+                            preg_quote($value['name'], '#') . '\1\s*\)#';
879
+
880
+                $rep   = array();
881
+                $rep[] = '\1\2=\3cid:' . $value['cid'] .'\3';
882
+                $rep[] = 'url(\1cid:' . $value['cid'] . '\1)';
883
+
884
+                $this->_htmlbody = preg_replace($regex, $rep, $this->_htmlbody);
885
+                $this->_html_images[$key]['name']
886
+                    = $this->_basename($this->_html_images[$key]['name']);
887
+            }
888
+        }
889
+
890
+        $this->_checkParams();
891
+
892
+        $null        = null;
893
+        $attachments = count($this->_parts)                 ? true : false;
894
+        $html_images = count($this->_html_images)           ? true : false;
895
+        $html        = strlen($this->_htmlbody)             ? true : false;
896
+        $text        = (!$html && strlen($this->_txtbody))  ? true : false;
897
+
898
+        switch (true) {
899
+        case $text && !$attachments:
900
+            $message =& $this->_addTextPart($null, $this->_txtbody);
901
+            break;
902
+
903
+        case !$text && !$html && $attachments:
904
+            $message =& $this->_addMixedPart();
905
+            for ($i = 0; $i < count($this->_parts); $i++) {
906
+                $this->_addAttachmentPart($message, $this->_parts[$i]);
907
+            }
908
+            break;
909
+
910
+        case $text && $attachments:
911
+            $message =& $this->_addMixedPart();
912
+            $this->_addTextPart($message, $this->_txtbody);
913
+            for ($i = 0; $i < count($this->_parts); $i++) {
914
+                $this->_addAttachmentPart($message, $this->_parts[$i]);
915
+            }
916
+            break;
917
+
918
+        case $html && !$attachments && !$html_images:
919
+            if (isset($this->_txtbody)) {
920
+                $message =& $this->_addAlternativePart($null);
921
+                $this->_addTextPart($message, $this->_txtbody);
922
+                $this->_addHtmlPart($message);
923
+            } else {
924
+                $message =& $this->_addHtmlPart($null);
925
+            }
926
+            break;
927
+
928
+        case $html && !$attachments && $html_images:
929
+            // * Content-Type: multipart/alternative;
930
+            //    * text
931
+            //    * Content-Type: multipart/related;
932
+            //       * html
933
+            //       * image...
934
+            if (isset($this->_txtbody)) {
935
+                $message =& $this->_addAlternativePart($null);
936
+                $this->_addTextPart($message, $this->_txtbody);
937
+
938
+                $ht =& $this->_addRelatedPart($message);
939
+                $this->_addHtmlPart($ht);
940
+                for ($i = 0; $i < count($this->_html_images); $i++) {
941
+                    $this->_addHtmlImagePart($ht, $this->_html_images[$i]);
942
+                }
943
+            } else {
944
+                // * Content-Type: multipart/related;
945
+                //    * html
946
+                //    * image...
947
+                $message =& $this->_addRelatedPart($null);
948
+                $this->_addHtmlPart($message);
949
+                for ($i = 0; $i < count($this->_html_images); $i++) {
950
+                    $this->_addHtmlImagePart($message, $this->_html_images[$i]);
951
+                }
952
+            }
953
+            /*
954
+            // #13444, #9725: the code below was a non-RFC compliant hack
955
+            // * Content-Type: multipart/related;
956
+            //    * Content-Type: multipart/alternative;
957
+            //        * text
958
+            //        * html
959
+            //    * image...
960
+            $message =& $this->_addRelatedPart($null);
961
+            if (isset($this->_txtbody)) {
962
+                $alt =& $this->_addAlternativePart($message);
963
+                $this->_addTextPart($alt, $this->_txtbody);
964
+                $this->_addHtmlPart($alt);
965
+            } else {
966
+                $this->_addHtmlPart($message);
967
+            }
968
+            for ($i = 0; $i < count($this->_html_images); $i++) {
969
+                $this->_addHtmlImagePart($message, $this->_html_images[$i]);
970
+            }
971
+            */
972
+            break;
973
+
974
+        case $html && $attachments && !$html_images:
975
+            $message =& $this->_addMixedPart();
976
+            if (isset($this->_txtbody)) {
977
+                $alt =& $this->_addAlternativePart($message);
978
+                $this->_addTextPart($alt, $this->_txtbody);
979
+                $this->_addHtmlPart($alt);
980
+            } else {
981
+                $this->_addHtmlPart($message);
982
+            }
983
+            for ($i = 0; $i < count($this->_parts); $i++) {
984
+                $this->_addAttachmentPart($message, $this->_parts[$i]);
985
+            }
986
+            break;
987
+
988
+        case $html && $attachments && $html_images:
989
+            $message =& $this->_addMixedPart();
990
+            if (isset($this->_txtbody)) {
991
+                $alt =& $this->_addAlternativePart($message);
992
+                $this->_addTextPart($alt, $this->_txtbody);
993
+                $rel =& $this->_addRelatedPart($alt);
994
+            } else {
995
+                $rel =& $this->_addRelatedPart($message);
996
+            }
997
+            $this->_addHtmlPart($rel);
998
+            for ($i = 0; $i < count($this->_html_images); $i++) {
999
+                $this->_addHtmlImagePart($rel, $this->_html_images[$i]);
1000
+            }
1001
+            for ($i = 0; $i < count($this->_parts); $i++) {
1002
+                $this->_addAttachmentPart($message, $this->_parts[$i]);
1003
+            }
1004
+            break;
1005
+
1006
+        }
1007
+
1008
+        if (!isset($message)) {
1009
+            $ret = null;
1010
+            return $ret;
1011
+        }
1012
+
1013
+        // Use saved boundary
1014
+        if (!empty($this->_build_params['boundary'])) {
1015
+            $boundary = $this->_build_params['boundary'];
1016
+        } else {
1017
+            $boundary = null;
1018
+        }
1019
+
1020
+        // Write output to file
1021
+        if ($filename) {
1022
+            // Append mimePart message headers and body into file
1023
+            $headers = $message->encodeToFile($filename, $boundary, $skip_head);
1024
+            if (PEAR::isError($headers)) {
1025
+                return $headers;
1026
+            }
1027
+            $this->_headers = array_merge($this->_headers, $headers);
1028
+            $ret = null;
1029
+            return $ret;
1030
+        } else {
1031
+            $output = $message->encode($boundary, $skip_head);
1032
+            if (PEAR::isError($output)) {
1033
+                return $output;
1034
+            }
1035
+            $this->_headers = array_merge($this->_headers, $output['headers']);
1036
+            $body = $output['body'];
1037
+            return $body;
1038
+        }
1039
+    }
1040
+
1041
+    /**
1042
+     * Returns an array with the headers needed to prepend to the email
1043
+     * (MIME-Version and Content-Type). Format of argument is:
1044
+     * $array['header-name'] = 'header-value';
1045
+     *
1046
+     * @param array $xtra_headers Assoc array with any extra headers (optional)
1047
+     *                            (Don't set Content-Type for multipart messages here!)
1048
+     * @param bool  $overwrite    Overwrite already existing headers.
1049
+     * @param bool  $skip_content Don't return content headers: Content-Type,
1050
+     *                            Content-Disposition and Content-Transfer-Encoding
1051
+     * 
1052
+     * @return array              Assoc array with the mime headers
1053
+     * @access public
1054
+     */
1055
+    function &headers($xtra_headers = null, $overwrite = false, $skip_content = false)
1056
+    {
1057
+        // Add mime version header
1058
+        $headers['MIME-Version'] = '1.0';
1059
+
1060
+        // Content-Type and Content-Transfer-Encoding headers should already
1061
+        // be present if get() was called, but we'll re-set them to make sure
1062
+        // we got them when called before get() or something in the message
1063
+        // has been changed after get() [#14780]
1064
+        if (!$skip_content) {
1065
+            $headers += $this->_contentHeaders();
1066
+        }
1067
+
1068
+        if (!empty($xtra_headers)) {
1069
+            $headers = array_merge($headers, $xtra_headers);
1070
+        }
1071
+
1072
+        if ($overwrite) {
1073
+            $this->_headers = array_merge($this->_headers, $headers);
1074
+        } else {
1075
+            $this->_headers = array_merge($headers, $this->_headers);
1076
+        }
1077
+
1078
+        $headers = $this->_headers;
1079
+
1080
+        if ($skip_content) {
1081
+            unset($headers['Content-Type']);
1082
+            unset($headers['Content-Transfer-Encoding']);
1083
+            unset($headers['Content-Disposition']);
1084
+        } else if (!empty($this->_build_params['ctype'])) {
1085
+            $headers['Content-Type'] = $this->_build_params['ctype'];
1086
+        }
1087
+
1088
+        $encodedHeaders = $this->_encodeHeaders($headers);
1089
+        return $encodedHeaders;
1090
+    }
1091
+
1092
+    /**
1093
+     * Get the text version of the headers
1094
+     * (useful if you want to use the PHP mail() function)
1095
+     *
1096
+     * @param array $xtra_headers Assoc array with any extra headers (optional)
1097
+     *                            (Don't set Content-Type for multipart messages here!)
1098
+     * @param bool  $overwrite    Overwrite the existing headers with new.
1099
+     * @param bool  $skip_content Don't return content headers: Content-Type,
1100
+     *                            Content-Disposition and Content-Transfer-Encoding
1101
+     *
1102
+     * @return string             Plain text headers
1103
+     * @access public
1104
+     */
1105
+    function txtHeaders($xtra_headers = null, $overwrite = false, $skip_content = false)
1106
+    {
1107
+        $headers = $this->headers($xtra_headers, $overwrite, $skip_content);
1108
+
1109
+        // Place Received: headers at the beginning of the message
1110
+        // Spam detectors often flag messages with it after the Subject: as spam
1111
+        if (isset($headers['Received'])) {
1112
+            $received = $headers['Received'];
1113
+            unset($headers['Received']);
1114
+            $headers = array('Received' => $received) + $headers;
1115
+        }
1116
+
1117
+        $ret = '';
1118
+        $eol = $this->_build_params['eol'];
1119
+
1120
+        foreach ($headers as $key => $val) {
1121
+            if (is_array($val)) {
1122
+                foreach ($val as $value) {
1123
+                    $ret .= "$key: $value" . $eol;
1124
+                }
1125
+            } else {
1126
+                $ret .= "$key: $val" . $eol;
1127
+            }
1128
+        }
1129
+
1130
+        return $ret;
1131
+    }
1132
+
1133
+    /**
1134
+     * Sets message Content-Type header.
1135
+     * Use it to build messages with various content-types e.g. miltipart/raport
1136
+     * not supported by _contentHeaders() function.
1137
+     *
1138
+     * @param string $type   Type name
1139
+     * @param array  $params Hash array of header parameters
1140
+     *
1141
+     * @return void
1142
+     * @access public
1143
+     * @since 1.7.0
1144
+     */
1145
+    function setContentType($type, $params = array())
1146
+    {
1147
+        $header = $type;
1148
+
1149
+        $eol = !empty($this->_build_params['eol'])
1150
+            ? $this->_build_params['eol'] : "\r\n";
1151
+
1152
+        // add parameters
1153
+        $token_regexp = '#([^\x21\x23-\x27\x2A\x2B\x2D'
1154
+            . '\x2E\x30-\x39\x41-\x5A\x5E-\x7E])#';
1155
+        if (is_array($params)) {
1156
+            foreach ($params as $name => $value) {
1157
+                if ($name == 'boundary') {
1158
+                    $this->_build_params['boundary'] = $value;
1159
+                }
1160
+                if (!preg_match($token_regexp, $value)) {
1161
+                    $header .= ";$eol $name=$value";
1162
+                } else {
1163
+                    $value = addcslashes($value, '\\"');
1164
+                    $header .= ";$eol $name=\"$value\"";
1165
+                }
1166
+            }
1167
+        }
1168
+
1169
+        // add required boundary parameter if not defined
1170
+        if (preg_match('/^multipart\//i', $type)) {
1171
+            if (empty($this->_build_params['boundary'])) {
1172
+                $this->_build_params['boundary'] = '=_' . md5(rand() . microtime());
1173
+            }
1174
+
1175
+            $header .= ";$eol boundary=\"".$this->_build_params['boundary']."\"";
1176
+        }
1177
+
1178
+        $this->_build_params['ctype'] = $header;
1179
+    }
1180
+
1181
+    /**
1182
+     * Sets the Subject header
1183
+     *
1184
+     * @param string $subject String to set the subject to.
1185
+     *
1186
+     * @return void
1187
+     * @access public
1188
+     */
1189
+    function setSubject($subject)
1190
+    {
1191
+        $this->_headers['Subject'] = $subject;
1192
+    }
1193
+
1194
+    /**
1195
+     * Set an email to the From (the sender) header
1196
+     *
1197
+     * @param string $email The email address to use
1198
+     *
1199
+     * @return void
1200
+     * @access public
1201
+     */
1202
+    function setFrom($email)
1203
+    {
1204
+        $this->_headers['From'] = $email;
1205
+    }
1206
+
1207
+    /**
1208
+     * Add an email to the To header
1209
+     * (multiple calls to this method are allowed)
1210
+     *
1211
+     * @param string $email The email direction to add
1212
+     *
1213
+     * @return void
1214
+     * @access public
1215
+     */
1216
+    function addTo($email)
1217
+    {
1218
+        if (isset($this->_headers['To'])) {
1219
+            $this->_headers['To'] .= ", $email";
1220
+        } else {
1221
+            $this->_headers['To'] = $email;
1222
+        }
1223
+    }
1224
+
1225
+    /**
1226
+     * Add an email to the Cc (carbon copy) header
1227
+     * (multiple calls to this method are allowed)
1228
+     *
1229
+     * @param string $email The email direction to add
1230
+     *
1231
+     * @return void
1232
+     * @access public
1233
+     */
1234
+    function addCc($email)
1235
+    {
1236
+        if (isset($this->_headers['Cc'])) {
1237
+            $this->_headers['Cc'] .= ", $email";
1238
+        } else {
1239
+            $this->_headers['Cc'] = $email;
1240
+        }
1241
+    }
1242
+
1243
+    /**
1244
+     * Add an email to the Bcc (blank carbon copy) header
1245
+     * (multiple calls to this method are allowed)
1246
+     *
1247
+     * @param string $email The email direction to add
1248
+     *
1249
+     * @return void
1250
+     * @access public
1251
+     */
1252
+    function addBcc($email)
1253
+    {
1254
+        if (isset($this->_headers['Bcc'])) {
1255
+            $this->_headers['Bcc'] .= ", $email";
1256
+        } else {
1257
+            $this->_headers['Bcc'] = $email;
1258
+        }
1259
+    }
1260
+
1261
+    /**
1262
+     * Since the PHP send function requires you to specify
1263
+     * recipients (To: header) separately from the other
1264
+     * headers, the To: header is not properly encoded.
1265
+     * To fix this, you can use this public method to 
1266
+     * encode your recipients before sending to the send
1267
+     * function
1268
+     *
1269
+     * @param string $recipients A comma-delimited list of recipients
1270
+     *
1271
+     * @return string            Encoded data
1272
+     * @access public
1273
+     */
1274
+    function encodeRecipients($recipients)
1275
+    {
1276
+        $input = array("To" => $recipients);
1277
+        $retval = $this->_encodeHeaders($input);
1278
+        return $retval["To"] ;
1279
+    }
1280
+
1281
+    /**
1282
+     * Encodes headers as per RFC2047
1283
+     *
1284
+     * @param array $input  The header data to encode
1285
+     * @param array $params Extra build parameters
1286
+     *
1287
+     * @return array        Encoded data
1288
+     * @access private
1289
+     */
1290
+    function _encodeHeaders($input, $params = array())
1291
+    {
1292
+        $build_params = $this->_build_params;
1293
+        while (list($key, $value) = each($params)) {
1294
+            $build_params[$key] = $value;
1295
+        }
1296
+
1297
+        foreach ($input as $hdr_name => $hdr_value) {
1298
+            if (is_array($hdr_value)) {
1299
+                foreach ($hdr_value as $idx => $value) {
1300
+                    $input[$hdr_name][$idx] = $this->encodeHeader(
1301
+                        $hdr_name, $value,
1302
+                        $build_params['head_charset'], $build_params['head_encoding']
1303
+                    );
1304
+                }
1305
+            } else {
1306
+                $input[$hdr_name] = $this->encodeHeader(
1307
+                    $hdr_name, $hdr_value,
1308
+                    $build_params['head_charset'], $build_params['head_encoding']
1309
+                );
1310
+            }
1311
+        }
1312
+
1313
+        return $input;
1314
+    }
1315
+
1316
+    /**
1317
+     * Encodes a header as per RFC2047
1318
+     *
1319
+     * @param string $name     The header name
1320
+     * @param string $value    The header data to encode
1321
+     * @param string $charset  Character set name
1322
+     * @param string $encoding Encoding name (base64 or quoted-printable)
1323
+     *
1324
+     * @return string          Encoded header data (without a name)
1325
+     * @access public
1326
+     * @since 1.5.3
1327
+     */
1328
+    function encodeHeader($name, $value, $charset, $encoding)
1329
+    {
1330
+        $mime_part = new Mail_mimePart;
1331
+        return $mime_part->encodeHeader(
1332
+            $name, $value, $charset, $encoding, $this->_build_params['eol']
1333
+        );
1334
+    }
1335
+
1336
+    /**
1337
+     * Get file's basename (locale independent) 
1338
+     *
1339
+     * @param string $filename Filename
1340
+     *
1341
+     * @return string          Basename
1342
+     * @access private
1343
+     */
1344
+    function _basename($filename)
1345
+    {
1346
+        // basename() is not unicode safe and locale dependent
1347
+        if (stristr(PHP_OS, 'win') || stristr(PHP_OS, 'netware')) {
1348
+            return preg_replace('/^.*[\\\\\\/]/', '', $filename);
1349
+        } else {
1350
+            return preg_replace('/^.*[\/]/', '', $filename);
1351
+        }
1352
+    }
1353
+
1354
+    /**
1355
+     * Get Content-Type and Content-Transfer-Encoding headers of the message
1356
+     *
1357
+     * @return array Headers array
1358
+     * @access private
1359
+     */
1360
+    function _contentHeaders()
1361
+    {
1362
+        $attachments = count($this->_parts)                 ? true : false;
1363
+        $html_images = count($this->_html_images)           ? true : false;
1364
+        $html        = strlen($this->_htmlbody)             ? true : false;
1365
+        $text        = (!$html && strlen($this->_txtbody))  ? true : false;
1366
+        $headers     = array();
1367
+
1368
+        // See get()
1369
+        switch (true) {
1370
+        case $text && !$attachments:
1371
+            $headers['Content-Type'] = 'text/plain';
1372
+            break;
1373
+
1374
+        case !$text && !$html && $attachments:
1375
+        case $text && $attachments:
1376
+        case $html && $attachments && !$html_images:
1377
+        case $html && $attachments && $html_images:
1378
+            $headers['Content-Type'] = 'multipart/mixed';
1379
+            break;
1380
+
1381
+        case $html && !$attachments && !$html_images && isset($this->_txtbody):
1382
+        case $html && !$attachments && $html_images && isset($this->_txtbody):
1383
+            $headers['Content-Type'] = 'multipart/alternative';
1384
+            break;
1385
+
1386
+        case $html && !$attachments && !$html_images && !isset($this->_txtbody):
1387
+            $headers['Content-Type'] = 'text/html';
1388
+            break;
1389
+
1390
+        case $html && !$attachments && $html_images && !isset($this->_txtbody):
1391
+            $headers['Content-Type'] = 'multipart/related';
1392
+            break;
1393
+
1394
+        default:
1395
+            return $headers;
1396
+        }
1397
+
1398
+        $this->_checkParams();
1399
+
1400
+        $eol = !empty($this->_build_params['eol'])
1401
+            ? $this->_build_params['eol'] : "\r\n";
1402
+
1403
+        if ($headers['Content-Type'] == 'text/plain') {
1404
+            // single-part message: add charset and encoding
1405
+            $charset = 'charset=' . $this->_build_params['text_charset'];
1406
+            // place charset parameter in the same line, if possible
1407
+            // 26 = strlen("Content-Type: text/plain; ")
1408
+            $headers['Content-Type']
1409
+                .= (strlen($charset) + 26 <= 76) ? "; $charset" : ";$eol $charset";
1410
+            $headers['Content-Transfer-Encoding']
1411
+                = $this->_build_params['text_encoding'];
1412
+        } else if ($headers['Content-Type'] == 'text/html') {
1413
+            // single-part message: add charset and encoding
1414
+            $charset = 'charset=' . $this->_build_params['html_charset'];
1415
+            // place charset parameter in the same line, if possible
1416
+            $headers['Content-Type']
1417
+                .= (strlen($charset) + 25 <= 76) ? "; $charset" : ";$eol $charset";
1418
+            $headers['Content-Transfer-Encoding']
1419
+                = $this->_build_params['html_encoding'];
1420
+        } else {
1421
+            // multipart message: and boundary
1422
+            if (!empty($this->_build_params['boundary'])) {
1423
+                $boundary = $this->_build_params['boundary'];
1424
+            } else if (!empty($this->_headers['Content-Type'])
1425
+                && preg_match('/boundary="([^"]+)"/', $this->_headers['Content-Type'], $m)
1426
+            ) {
1427
+                $boundary = $m[1];
1428
+            } else {
1429
+                $boundary = '=_' . md5(rand() . microtime());
1430
+            }
1431
+
1432
+            $this->_build_params['boundary'] = $boundary;
1433
+            $headers['Content-Type'] .= ";$eol boundary=\"$boundary\"";
1434
+        }
1435
+
1436
+        return $headers;
1437
+    }
1438
+
1439
+    /**
1440
+     * Validate and set build parameters
1441
+     *
1442
+     * @return void
1443
+     * @access private
1444
+     */
1445
+    function _checkParams()
1446
+    {
1447
+        $encodings = array('7bit', '8bit', 'base64', 'quoted-printable');
1448
+
1449
+        $this->_build_params['text_encoding']
1450
+            = strtolower($this->_build_params['text_encoding']);
1451
+        $this->_build_params['html_encoding']
1452
+            = strtolower($this->_build_params['html_encoding']);
1453
+
1454
+        if (!in_array($this->_build_params['text_encoding'], $encodings)) {
1455
+            $this->_build_params['text_encoding'] = '7bit';
1456
+        }
1457
+        if (!in_array($this->_build_params['html_encoding'], $encodings)) {
1458
+            $this->_build_params['html_encoding'] = '7bit';
1459
+        }
1460
+
1461
+        // text body
1462
+        if ($this->_build_params['text_encoding'] == '7bit'
1463
+            && !preg_match('/ascii/i', $this->_build_params['text_charset'])
1464
+            && preg_match('/[^\x00-\x7F]/', $this->_txtbody)
1465
+        ) {
1466
+            $this->_build_params['text_encoding'] = 'quoted-printable';
1467
+        }
1468
+        // html body
1469
+        if ($this->_build_params['html_encoding'] == '7bit'
1470
+            && !preg_match('/ascii/i', $this->_build_params['html_charset'])
1471
+            && preg_match('/[^\x00-\x7F]/', $this->_htmlbody)
1472
+        ) {
1473
+            $this->_build_params['html_encoding'] = 'quoted-printable';
1474
+        }
1475
+    }
1476
+
1477
+} // End of class
1478
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Mail/mimeDecode.php Added
1005
 
1
@@ -0,0 +1,1003 @@
2
+<?php
3
+/**
4
+ * The Mail_mimeDecode class is used to decode mail/mime messages
5
+ *
6
+ * This class will parse a raw mime email and return
7
+ * the structure. Returned structure is similar to
8
+ * that returned by imap_fetchstructure().
9
+ *
10
+ *  +----------------------------- IMPORTANT ------------------------------+
11
+ *  | Usage of this class compared to native php extensions such as        |
12
+ *  | mailparse or imap, is slow and may be feature deficient. If available|
13
+ *  | you are STRONGLY recommended to use the php extensions.              |
14
+ *  +----------------------------------------------------------------------+
15
+ *
16
+ * Compatible with PHP versions 4 and 5
17
+ *
18
+ * LICENSE: This LICENSE is in the BSD license style.
19
+ * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org>
20
+ * Copyright (c) 2003-2006, PEAR <pear-group@php.net>
21
+ * All rights reserved.
22
+ *
23
+ * Redistribution and use in source and binary forms, with or
24
+ * without modification, are permitted provided that the following
25
+ * conditions are met:
26
+ *
27
+ * - Redistributions of source code must retain the above copyright
28
+ *   notice, this list of conditions and the following disclaimer.
29
+ * - Redistributions in binary form must reproduce the above copyright
30
+ *   notice, this list of conditions and the following disclaimer in the
31
+ *   documentation and/or other materials provided with the distribution.
32
+ * - Neither the name of the authors, nor the names of its contributors 
33
+ *   may be used to endorse or promote products derived from this 
34
+ *   software without specific prior written permission.
35
+ *
36
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
37
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
38
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
39
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
40
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
41
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
42
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
43
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
44
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
45
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
46
+ * THE POSSIBILITY OF SUCH DAMAGE.
47
+ *
48
+ * @category   Mail
49
+ * @package    Mail_Mime
50
+ * @author     Richard Heyes  <richard@phpguru.org>
51
+ * @author     George Schlossnagle <george@omniti.com>
52
+ * @author     Cipriano Groenendal <cipri@php.net>
53
+ * @author     Sean Coates <sean@php.net>
54
+ * @copyright  2003-2006 PEAR <pear-group@php.net>
55
+ * @license    http://www.opensource.org/licenses/bsd-license.php BSD License
56
+ * @version    CVS: $Id$
57
+ * @link       http://pear.php.net/package/Mail_mime
58
+ */
59
+
60
+
61
+/**
62
+ * require PEAR
63
+ *
64
+ * This package depends on PEAR to raise errors.
65
+ */
66
+require_once 'PEAR.php';
67
+
68
+
69
+/**
70
+ * The Mail_mimeDecode class is used to decode mail/mime messages
71
+ *
72
+ * This class will parse a raw mime email and return the structure.
73
+ * Returned structure is similar to that returned by imap_fetchstructure().
74
+ *
75
+ *  +----------------------------- IMPORTANT ------------------------------+
76
+ *  | Usage of this class compared to native php extensions such as        |
77
+ *  | mailparse or imap, is slow and may be feature deficient. If available|
78
+ *  | you are STRONGLY recommended to use the php extensions.              |
79
+ *  +----------------------------------------------------------------------+
80
+ *
81
+ * @category   Mail
82
+ * @package    Mail_Mime
83
+ * @author     Richard Heyes  <richard@phpguru.org>
84
+ * @author     George Schlossnagle <george@omniti.com>
85
+ * @author     Cipriano Groenendal <cipri@php.net>
86
+ * @author     Sean Coates <sean@php.net>
87
+ * @copyright  2003-2006 PEAR <pear-group@php.net>
88
+ * @license    http://www.opensource.org/licenses/bsd-license.php BSD License
89
+ * @version    Release: @package_version@
90
+ * @link       http://pear.php.net/package/Mail_mime
91
+ */
92
+class Mail_mimeDecode extends PEAR
93
+{
94
+    /**
95
+     * The raw email to decode
96
+     *
97
+     * @var    string
98
+     * @access private
99
+     */
100
+    var $_input;
101
+
102
+    /**
103
+     * The header part of the input
104
+     *
105
+     * @var    string
106
+     * @access private
107
+     */
108
+    var $_header;
109
+
110
+    /**
111
+     * The body part of the input
112
+     *
113
+     * @var    string
114
+     * @access private
115
+     */
116
+    var $_body;
117
+
118
+    /**
119
+     * If an error occurs, this is used to store the message
120
+     *
121
+     * @var    string
122
+     * @access private
123
+     */
124
+    var $_error;
125
+
126
+    /**
127
+     * Flag to determine whether to include bodies in the
128
+     * returned object.
129
+     *
130
+     * @var    boolean
131
+     * @access private
132
+     */
133
+    var $_include_bodies;
134
+
135
+    /**
136
+     * Flag to determine whether to decode bodies
137
+     *
138
+     * @var    boolean
139
+     * @access private
140
+     */
141
+    var $_decode_bodies;
142
+
143
+    /**
144
+     * Flag to determine whether to decode headers
145
+     *
146
+     * @var    boolean
147
+     * @access private
148
+     */
149
+    var $_decode_headers;
150
+
151
+    /**
152
+     * Flag to determine whether to include attached messages
153
+     * as body in the returned object. Depends on $_include_bodies
154
+     *
155
+     * @var    boolean
156
+     * @access private
157
+     */
158
+    var $_rfc822_bodies;
159
+
160
+    /**
161
+     * Constructor.
162
+     *
163
+     * Sets up the object, initialise the variables, and splits and
164
+     * stores the header and body of the input.
165
+     *
166
+     * @param string The input to decode
167
+     * @access public
168
+     */
169
+    function Mail_mimeDecode($input)
170
+    {
171
+        list($header, $body)   = $this->_splitBodyHeader($input);
172
+
173
+        $this->_input          = $input;
174
+        $this->_header         = $header;
175
+        $this->_body           = $body;
176
+        $this->_decode_bodies  = false;
177
+        $this->_include_bodies = true;
178
+        $this->_rfc822_bodies  = false;
179
+    }
180
+
181
+    /**
182
+     * Begins the decoding process. If called statically
183
+     * it will create an object and call the decode() method
184
+     * of it.
185
+     *
186
+     * @param array An array of various parameters that determine
187
+     *              various things:
188
+     *              include_bodies - Whether to include the body in the returned
189
+     *                               object.
190
+     *              decode_bodies  - Whether to decode the bodies
191
+     *                               of the parts. (Transfer encoding)
192
+     *              decode_headers - Whether to decode headers
193
+     *              input          - If called statically, this will be treated
194
+     *                               as the input
195
+     * @return object Decoded results
196
+     * @access public
197
+     */
198
+    function decode($params = null)
199
+    {
200
+        // determine if this method has been called statically
201
+        $isStatic = empty($this) || !is_a($this, __CLASS__);
202
+
203
+        // Have we been called statically?
204
+   // If so, create an object and pass details to that.
205
+        if ($isStatic AND isset($params['input'])) {
206
+
207
+            $obj = new Mail_mimeDecode($params['input']);
208
+            $structure = $obj->decode($params);
209
+
210
+        // Called statically but no input
211
+        } elseif ($isStatic) {
212
+            return PEAR::raiseError('Called statically and no input given');
213
+
214
+        // Called via an object
215
+        } else {
216
+            $this->_include_bodies = isset($params['include_bodies']) ?
217
+                                $params['include_bodies'] : false;
218
+            $this->_decode_bodies  = isset($params['decode_bodies']) ?
219
+                                $params['decode_bodies']  : false;
220
+            $this->_decode_headers = isset($params['decode_headers']) ?
221
+                                $params['decode_headers'] : false;
222
+            $this->_rfc822_bodies  = isset($params['rfc_822bodies']) ?
223
+                                $params['rfc_822bodies']  : false;
224
+
225
+            $structure = $this->_decode($this->_header, $this->_body);
226
+            if ($structure === false) {
227
+                $structure = $this->raiseError($this->_error);
228
+            }
229
+        }
230
+
231
+        return $structure;
232
+    }
233
+
234
+    /**
235
+     * Performs the decoding. Decodes the body string passed to it
236
+     * If it finds certain content-types it will call itself in a
237
+     * recursive fashion
238
+     *
239
+     * @param string Header section
240
+     * @param string Body section
241
+     * @return object Results of decoding process
242
+     * @access private
243
+     */
244
+    function _decode($headers, $body, $default_ctype = 'text/plain')
245
+    {
246
+        $return = new stdClass;
247
+        $return->headers = array();
248
+        $headers = $this->_parseHeaders($headers);
249
+
250
+        foreach ($headers as $value) {
251
+            $value['value'] = $this->_decode_headers ? $this->_decodeHeader($value['value']) : $value['value'];
252
+            if (isset($return->headers[strtolower($value['name'])]) AND !is_array($return->headers[strtolower($value['name'])])) {
253
+                $return->headers[strtolower($value['name'])]   = array($return->headers[strtolower($value['name'])]);
254
+                $return->headers[strtolower($value['name'])][] = $value['value'];
255
+
256
+            } elseif (isset($return->headers[strtolower($value['name'])])) {
257
+                $return->headers[strtolower($value['name'])][] = $value['value'];
258
+
259
+            } else {
260
+                $return->headers[strtolower($value['name'])] = $value['value'];
261
+            }
262
+        }
263
+
264
+
265
+        foreach ($headers as $key => $value) {
266
+            $headers[$key]['name'] = strtolower($headers[$key]['name']);
267
+            switch ($headers[$key]['name']) {
268
+
269
+                case 'content-type':
270
+                    $content_type = $this->_parseHeaderValue($headers[$key]['value']);
271
+
272
+                    if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) {
273
+                        $return->ctype_primary   = $regs[1];
274
+                        $return->ctype_secondary = $regs[2];
275
+                    }
276
+
277
+                    if (isset($content_type['other'])) {
278
+                        foreach($content_type['other'] as $p_name => $p_value) {
279
+                            $return->ctype_parameters[$p_name] = $p_value;
280
+                        }
281
+                    }
282
+                    break;
283
+
284
+                case 'content-disposition':
285
+                    $content_disposition = $this->_parseHeaderValue($headers[$key]['value']);
286
+                    $return->disposition   = $content_disposition['value'];
287
+                    if (isset($content_disposition['other'])) {
288
+                        foreach($content_disposition['other'] as $p_name => $p_value) {
289
+                            $return->d_parameters[$p_name] = $p_value;
290
+                        }
291
+                    }
292
+                    break;
293
+
294
+                case 'content-transfer-encoding':
295
+                    $content_transfer_encoding = $this->_parseHeaderValue($headers[$key]['value']);
296
+                    break;
297
+            }
298
+        }
299
+
300
+        if (isset($content_type)) {
301
+            switch (strtolower($content_type['value'])) {
302
+                case 'text/plain':
303
+                    $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
304
+                    $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null;
305
+                    break;
306
+
307
+                case 'text/html':
308
+                    $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
309
+                    $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null;
310
+                    break;
311
+
312
+                case 'multipart/parallel':
313
+                case 'multipart/appledouble': // Appledouble mail
314
+                case 'multipart/report': // RFC1892
315
+                case 'multipart/signed': // PGP
316
+                case 'multipart/digest':
317
+                case 'multipart/alternative':
318
+                case 'multipart/related':
319
+                case 'multipart/mixed':
320
+                case 'application/vnd.wap.multipart.related':
321
+                    if(!isset($content_type['other']['boundary'])){
322
+                        $this->_error = 'No boundary found for ' . $content_type['value'] . ' part';
323
+                        return false;
324
+                    }
325
+
326
+                    $default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain';
327
+
328
+                    $parts = $this->_boundarySplit($body, $content_type['other']['boundary']);
329
+                    for ($i = 0; $i < count($parts); $i++) {
330
+                        list($part_header, $part_body) = $this->_splitBodyHeader($parts[$i]);
331
+                        $part = $this->_decode($part_header, $part_body, $default_ctype);
332
+                        if($part === false)
333
+                            $part = $this->raiseError($this->_error);
334
+                        $return->parts[] = $part;
335
+                    }
336
+                    break;
337
+
338
+                case 'message/rfc822':
339
+                   if ($this->_rfc822_bodies) {
340
+                       $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
341
+                       $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body);
342
+                   }
343
+                    $obj = new Mail_mimeDecode($body);
344
+                    $return->parts[] = $obj->decode(array('include_bodies' => $this->_include_bodies,
345
+                                                         'decode_bodies'  => $this->_decode_bodies,
346
+                                                         'decode_headers' => $this->_decode_headers));
347
+                    unset($obj);
348
+                    break;
349
+
350
+                default:
351
+                    if(!isset($content_transfer_encoding['value']))
352
+                        $content_transfer_encoding['value'] = '7bit';
353
+                    $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value']) : $body) : null;
354
+                    break;
355
+            }
356
+
357
+        } else {
358
+            $ctype = explode('/', $default_ctype);
359
+            $return->ctype_primary   = $ctype[0];
360
+            $return->ctype_secondary = $ctype[1];
361
+            $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body) : $body) : null;
362
+        }
363
+
364
+        return $return;
365
+    }
366
+
367
+    /**
368
+     * Given the output of the above function, this will return an
369
+     * array of references to the parts, indexed by mime number.
370
+     *
371
+     * @param  object $structure   The structure to go through
372
+     * @param  string $mime_number Internal use only.
373
+     * @return array               Mime numbers
374
+     */
375
+    function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prepend = '')
376
+    {
377
+        $return = array();
378
+        if (!empty($structure->parts)) {
379
+            if ($mime_number != '') {
380
+                $structure->mime_id = $prepend . $mime_number;
381
+                $return[$prepend . $mime_number] = &$structure;
382
+            }
383
+            for ($i = 0; $i < count($structure->parts); $i++) {
384
+
385
+            
386
+                if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') {
387
+                    $prepend      = $prepend . $mime_number . '.';
388
+                    $_mime_number = '';
389
+                } else {
390
+                    $_mime_number = ($mime_number == '' ? $i + 1 : sprintf('%s.%s', $mime_number, $i + 1));
391
+                }
392
+
393
+                $arr = &Mail_mimeDecode::getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend);
394
+                foreach ($arr as $key => $val) {
395
+                    $no_refs ? $return[$key] = '' : $return[$key] = &$arr[$key];
396
+                }
397
+            }
398
+        } else {
399
+            if ($mime_number == '') {
400
+                $mime_number = '1';
401
+            }
402
+            $structure->mime_id = $prepend . $mime_number;
403
+            $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure;
404
+        }
405
+        
406
+        return $return;
407
+    }
408
+
409
+    /**
410
+     * Given a string containing a header and body
411
+     * section, this function will split them (at the first
412
+     * blank line) and return them.
413
+     *
414
+     * @param string Input to split apart
415
+     * @return array Contains header and body section
416
+     * @access private
417
+     */
418
+    function _splitBodyHeader($input)
419
+    {
420
+        if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) {
421
+            return array($match[1], $match[2]);
422
+        }
423
+        // bug #17325 - empty bodies are allowed. - we just check that at least one line 
424
+        // of headers exist..
425
+        if (count(explode("\n",$input))) {
426
+            return array($input, '');
427
+        }
428
+        $this->_error = 'Could not split header and body';
429
+        return false;
430
+    }
431
+
432
+    /**
433
+     * Parse headers given in $input and return
434
+     * as assoc array.
435
+     *
436
+     * @param string Headers to parse
437
+     * @return array Contains parsed headers
438
+     * @access private
439
+     */
440
+    function _parseHeaders($input)
441
+    {
442
+
443
+        if ($input !== '') {
444
+            // Unfold the input
445
+            $input   = preg_replace("/\r?\n/", "\r\n", $input);
446
+            //#7065 - wrapping.. with encoded stuff.. - probably not needed,
447
+            // wrapping space should only get removed if the trailing item on previous line is a 
448
+            // encoded character
449
+            $input   = preg_replace("/=\r\n(\t| )+/", '=', $input);
450
+            $input   = preg_replace("/\r\n(\t| )+/", ' ', $input);
451
+            
452
+            $headers = explode("\r\n", trim($input));
453
+
454
+            foreach ($headers as $value) {
455
+                $hdr_name = substr($value, 0, $pos = strpos($value, ':'));
456
+                $hdr_value = substr($value, $pos+1);
457
+                if($hdr_value[0] == ' ')
458
+                    $hdr_value = substr($hdr_value, 1);
459
+
460
+                $return[] = array(
461
+                                  'name'  => $hdr_name,
462
+                                  'value' =>  $hdr_value
463
+                                 );
464
+            }
465
+        } else {
466
+            $return = array();
467
+        }
468
+
469
+        return $return;
470
+    }
471
+
472
+    /**
473
+     * Function to parse a header value,
474
+     * extract first part, and any secondary
475
+     * parts (after ;) This function is not as
476
+     * robust as it could be. Eg. header comments
477
+     * in the wrong place will probably break it.
478
+     *
479
+     * @param string Header value to parse
480
+     * @return array Contains parsed result
481
+     * @access private
482
+     */
483
+    function _parseHeaderValue($input)
484
+    {
485
+
486
+        if (($pos = strpos($input, ';')) === false) {
487
+            $input = $this->_decode_headers ? $this->_decodeHeader($input) : $input;
488
+            $return['value'] = trim($input);
489
+            return $return;
490
+        }
491
+
492
+
493
+
494
+        $value = substr($input, 0, $pos);
495
+        $value = $this->_decode_headers ? $this->_decodeHeader($value) : $value;
496
+        $return['value'] = trim($value);
497
+        $input = trim(substr($input, $pos+1));
498
+
499
+        if (!strlen($input) > 0) {
500
+            return $return;
501
+        }
502
+        // at this point input contains xxxx=".....";zzzz="...."
503
+        // since we are dealing with quoted strings, we need to handle this properly..
504
+        $i = 0;
505
+        $l = strlen($input);
506
+        $key = '';
507
+        $val = false; // our string - including quotes..
508
+        $q = false; // in quote..
509
+        $lq = ''; // last quote..
510
+
511
+        while ($i < $l) {
512
+            
513
+            $c = $input[$i];
514
+            //var_dump(array('i'=>$i,'c'=>$c,'q'=>$q, 'lq'=>$lq, 'key'=>$key, 'val' =>$val));
515
+
516
+            $escaped = false;
517
+            if ($c == '\\') {
518
+                $i++;
519
+                if ($i == $l-1) { // end of string.
520
+                    break;
521
+                }
522
+                $escaped = true;
523
+                $c = $input[$i];
524
+            }            
525
+
526
+
527
+            // state - in key..
528
+            if ($val === false) {
529
+                if (!$escaped && $c == '=') {
530
+                    $val = '';
531
+                    $key = trim($key);
532
+                    $i++;
533
+                    continue;
534
+                }
535
+                if (!$escaped && $c == ';') {
536
+                    if ($key) { // a key without a value..
537
+                        $key= trim($key);
538
+                        $return['other'][$key] = '';
539
+                        $return['other'][strtolower($key)] = '';
540
+                    }
541
+                    $key = '';
542
+                }
543
+                $key .= $c;
544
+                $i++;
545
+                continue;
546
+            }
547
+                     
548
+            // state - in value.. (as $val is set..)
549
+
550
+            if ($q === false) {
551
+                // not in quote yet.
552
+                if ((!strlen($val) || $lq !== false) && $c == ' ' ||  $c == "\t") {
553
+                    $i++;
554
+                    continue; // skip leading spaces after '=' or after '"'
555
+                }
556
+                if (!$escaped && ($c == '"' || $c == "'")) {
557
+                    // start quoted area..
558
+                    $q = $c;
559
+                    // in theory should not happen raw text in value part..
560
+                    // but we will handle it as a merged part of the string..
561
+                    $val = !strlen(trim($val)) ? '' : trim($val);
562
+                    $i++;
563
+                    continue;
564
+                }
565
+                // got end....
566
+                if (!$escaped && $c == ';') {
567
+
568
+                    $val = trim($val);
569
+                    $added = false;
570
+                    if (preg_match('/\*[0-9]+$/', $key)) {
571
+                        // this is the extended aaa*0=...;aaa*1=.... code
572
+                        // it assumes the pieces arrive in order, and are valid...
573
+                        $key = preg_replace('/\*[0-9]+$/', '', $key);
574
+                        if (isset($return['other'][$key])) {
575
+                            $return['other'][$key] .= $val;
576
+                            if (strtolower($key) != $key) {
577
+                                $return['other'][strtolower($key)] .= $val;
578
+                            }
579
+                            $added = true;
580
+                        }
581
+                        // continue and use standard setters..
582
+                    }
583
+                    if (!$added) {
584
+                        $return['other'][$key] = $val;
585
+                        $return['other'][strtolower($key)] = $val;
586
+                    }
587
+                    $val = false;
588
+                    $key = '';
589
+                    $lq = false;
590
+                    $i++;
591
+                    continue;
592
+                }
593
+
594
+                $val .= $c;
595
+                $i++;
596
+                continue;
597
+            }
598
+            
599
+            // state - in quote..
600
+            if (!$escaped && $c == $q) {  // potential exit state..
601
+
602
+                // end of quoted string..
603
+                $lq = $q;
604
+                $q = false;
605
+                $i++;
606
+                continue;
607
+            }
608
+                
609
+            // normal char inside of quoted string..
610
+            $val.= $c;
611
+            $i++;
612
+        }
613
+        
614
+        // do we have anything left..
615
+        if (strlen(trim($key)) || $val !== false) {
616
+           
617
+            $val = trim($val);
618
+            $added = false;
619
+            if ($val !== false && preg_match('/\*[0-9]+$/', $key)) {
620
+                // no dupes due to our crazy regexp.
621
+                $key = preg_replace('/\*[0-9]+$/', '', $key);
622
+                if (isset($return['other'][$key])) {
623
+                    $return['other'][$key] .= $val;
624
+                    if (strtolower($key) != $key) {
625
+                        $return['other'][strtolower($key)] .= $val;
626
+                    }
627
+                    $added = true;
628
+                }
629
+                // continue and use standard setters..
630
+            }
631
+            if (!$added) {
632
+                $return['other'][$key] = $val;
633
+                $return['other'][strtolower($key)] = $val;
634
+            }
635
+        }
636
+        // decode values.
637
+        foreach($return['other'] as $key =>$val) {
638
+            $return['other'][$key] = $this->_decode_headers ? $this->_decodeHeader($val) : $val;
639
+        }
640
+       //print_r($return);
641
+        return $return;
642
+    }
643
+
644
+    /**
645
+     * This function splits the input based
646
+     * on the given boundary
647
+     *
648
+     * @param string Input to parse
649
+     * @return array Contains array of resulting mime parts
650
+     * @access private
651
+     */
652
+    function _boundarySplit($input, $boundary)
653
+    {
654
+        $parts = array();
655
+
656
+        $bs_possible = substr($boundary, 2, -2);
657
+        $bs_check = '\"' . $bs_possible . '\"';
658
+
659
+        if ($boundary == $bs_check) {
660
+            $boundary = $bs_possible;
661
+        }
662
+        $tmp = preg_split("/--".preg_quote($boundary, '/')."((?=\s)|--)/", $input);
663
+
664
+        $len = count($tmp) -1;
665
+        for ($i = 1; $i < $len; $i++) {
666
+            if (strlen(trim($tmp[$i]))) {
667
+                $parts[] = $tmp[$i];
668
+            }
669
+        }
670
+        
671
+        // add the last part on if it does not end with the 'closing indicator'
672
+        if (!empty($tmp[$len]) && strlen(trim($tmp[$len])) && $tmp[$len][0] != '-') {
673
+            $parts[] = $tmp[$len];
674
+        }
675
+        return $parts;
676
+    }
677
+
678
+    /**
679
+     * Given a header, this function will decode it
680
+     * according to RFC2047. Probably not *exactly*
681
+     * conformant, but it does pass all the given
682
+     * examples (in RFC2047).
683
+     *
684
+     * @param string Input header value to decode
685
+     * @return string Decoded header value
686
+     * @access private
687
+     */
688
+    function _decodeHeader($input)
689
+    {
690
+        // Remove white space between encoded-words
691
+        $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input);
692
+
693
+        // For each encoded-word...
694
+        while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) {
695
+
696
+            $encoded  = $matches[1];
697
+            $charset  = $matches[2];
698
+            $encoding = $matches[3];
699
+            $text     = $matches[4];
700
+
701
+            switch (strtolower($encoding)) {
702
+                case 'b':
703
+                    $text = base64_decode($text);
704
+                    break;
705
+
706
+                case 'q':
707
+                    $text = str_replace('_', ' ', $text);
708
+                    preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
709
+                    foreach($matches[1] as $value)
710
+                        $text = str_replace('='.$value, chr(hexdec($value)), $text);
711
+                    break;
712
+            }
713
+
714
+            $input = str_replace($encoded, $text, $input);
715
+        }
716
+
717
+        return $input;
718
+    }
719
+
720
+    /**
721
+     * Given a body string and an encoding type,
722
+     * this function will decode and return it.
723
+     *
724
+     * @param  string Input body to decode
725
+     * @param  string Encoding type to use.
726
+     * @return string Decoded body
727
+     * @access private
728
+     */
729
+    function _decodeBody($input, $encoding = '7bit')
730
+    {
731
+        switch (strtolower($encoding)) {
732
+            case '7bit':
733
+                return $input;
734
+                break;
735
+
736
+            case 'quoted-printable':
737
+                return $this->_quotedPrintableDecode($input);
738
+                break;
739
+
740
+            case 'base64':
741
+                return base64_decode($input);
742
+                break;
743
+
744
+            default:
745
+                return $input;
746
+        }
747
+    }
748
+
749
+    /**
750
+     * Given a quoted-printable string, this
751
+     * function will decode and return it.
752
+     *
753
+     * @param  string Input body to decode
754
+     * @return string Decoded body
755
+     * @access private
756
+     */
757
+    function _quotedPrintableDecode($input)
758
+    {
759
+        // Remove soft line breaks
760
+        $input = preg_replace("/=\r?\n/", '', $input);
761
+
762
+        // Replace encoded characters
763
+       $input = preg_replace('/=([a-f0-9]{2})/ie', "chr(hexdec('\\1'))", $input);
764
+
765
+        return $input;
766
+    }
767
+
768
+    /**
769
+     * Checks the input for uuencoded files and returns
770
+     * an array of them. Can be called statically, eg:
771
+     *
772
+     * $files =& Mail_mimeDecode::uudecode($some_text);
773
+     *
774
+     * It will check for the begin 666 ... end syntax
775
+     * however and won't just blindly decode whatever you
776
+     * pass it.
777
+     *
778
+     * @param  string Input body to look for attahcments in
779
+     * @return array  Decoded bodies, filenames and permissions
780
+     * @access public
781
+     * @author Unknown
782
+     */
783
+    function &uudecode($input)
784
+    {
785
+        // Find all uuencoded sections
786
+        preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches);
787
+
788
+        for ($j = 0; $j < count($matches[3]); $j++) {
789
+
790
+            $str      = $matches[3][$j];
791
+            $filename = $matches[2][$j];
792
+            $fileperm = $matches[1][$j];
793
+
794
+            $file = '';
795
+            $str = preg_split("/\r?\n/", trim($str));
796
+            $strlen = count($str);
797
+
798
+            for ($i = 0; $i < $strlen; $i++) {
799
+                $pos = 1;
800
+                $d = 0;
801
+                $len=(int)(((ord(substr($str[$i],0,1)) -32) - ' ') & 077);
802
+
803
+                while (($d + 3 <= $len) AND ($pos + 4 <= strlen($str[$i]))) {
804
+                    $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
805
+                    $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
806
+                    $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20);
807
+                    $c3 = (ord(substr($str[$i],$pos+3,1)) ^ 0x20);
808
+                    $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
809
+
810
+                    $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2));
811
+
812
+                    $file .= chr(((($c2 - ' ') & 077) << 6) |  (($c3 - ' ') & 077));
813
+
814
+                    $pos += 4;
815
+                    $d += 3;
816
+                }
817
+
818
+                if (($d + 2 <= $len) && ($pos + 3 <= strlen($str[$i]))) {
819
+                    $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
820
+                    $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
821
+                    $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20);
822
+                    $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
823
+
824
+                    $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2));
825
+
826
+                    $pos += 3;
827
+                    $d += 2;
828
+                }
829
+
830
+                if (($d + 1 <= $len) && ($pos + 2 <= strlen($str[$i]))) {
831
+                    $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20);
832
+                    $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20);
833
+                    $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
834
+
835
+                }
836
+            }
837
+            $files[] = array('filename' => $filename, 'fileperm' => $fileperm, 'filedata' => $file);
838
+        }
839
+
840
+        return $files;
841
+    }
842
+
843
+    /**
844
+     * getSendArray() returns the arguments required for Mail::send()
845
+     * used to build the arguments for a mail::send() call 
846
+     *
847
+     * Usage:
848
+     * $mailtext = Full email (for example generated by a template)
849
+     * $decoder = new Mail_mimeDecode($mailtext);
850
+     * $parts =  $decoder->getSendArray();
851
+     * if (!PEAR::isError($parts) {
852
+     *     list($recipents,$headers,$body) = $parts;
853
+     *     $mail = Mail::factory('smtp');
854
+     *     $mail->send($recipents,$headers,$body);
855
+     * } else {
856
+     *     echo $parts->message;
857
+     * }
858
+     * @return mixed   array of recipeint, headers,body or Pear_Error
859
+     * @access public
860
+     * @author Alan Knowles <alan@akbkhome.com>
861
+     */
862
+    function getSendArray()
863
+    {
864
+        // prevent warning if this is not set
865
+        $this->_decode_headers = FALSE;
866
+        $headerlist =$this->_parseHeaders($this->_header);
867
+        $to = "";
868
+        if (!$headerlist) {
869
+            return $this->raiseError("Message did not contain headers");
870
+        }
871
+        foreach($headerlist as $item) {
872
+            $header[$item['name']] = $item['value'];
873
+            switch (strtolower($item['name'])) {
874
+                case "to":
875
+                case "cc":
876
+                case "bcc":
877
+                    $to .= ",".$item['value'];
878
+                default:
879
+                   break;
880
+            }
881
+        }
882
+        if ($to == "") {
883
+            return $this->raiseError("Message did not contain any recipents");
884
+        }
885
+        $to = substr($to,1);
886
+        return array($to,$header,$this->_body);
887
+    } 
888
+
889
+    /**
890
+     * Returns a xml copy of the output of
891
+     * Mail_mimeDecode::decode. Pass the output in as the
892
+     * argument. This function can be called statically. Eg:
893
+     *
894
+     * $output = $obj->decode();
895
+     * $xml    = Mail_mimeDecode::getXML($output);
896
+     *
897
+     * The DTD used for this should have been in the package. Or
898
+     * alternatively you can get it from cvs, or here:
899
+     * http://www.phpguru.org/xmail/xmail.dtd.
900
+     *
901
+     * @param  object Input to convert to xml. This should be the
902
+     *                output of the Mail_mimeDecode::decode function
903
+     * @return string XML version of input
904
+     * @access public
905
+     */
906
+    function getXML($input)
907
+    {
908
+        $crlf    =  "\r\n";
909
+        $output  = '<?xml version=\'1.0\'?>' . $crlf .
910
+                   '<!DOCTYPE email SYSTEM "http://www.phpguru.org/xmail/xmail.dtd">' . $crlf .
911
+                   '<email>' . $crlf .
912
+                   Mail_mimeDecode::_getXML($input) .
913
+                   '</email>';
914
+
915
+        return $output;
916
+    }
917
+
918
+    /**
919
+     * Function that does the actual conversion to xml. Does a single
920
+     * mimepart at a time.
921
+     *
922
+     * @param  object  Input to convert to xml. This is a mimepart object.
923
+     *                 It may or may not contain subparts.
924
+     * @param  integer Number of tabs to indent
925
+     * @return string  XML version of input
926
+     * @access private
927
+     */
928
+    function _getXML($input, $indent = 1)
929
+    {
930
+        $htab    =  "\t";
931
+        $crlf    =  "\r\n";
932
+        $output  =  '';
933
+        $headers = @(array)$input->headers;
934
+
935
+        foreach ($headers as $hdr_name => $hdr_value) {
936
+
937
+            // Multiple headers with this name
938
+            if (is_array($headers[$hdr_name])) {
939
+                for ($i = 0; $i < count($hdr_value); $i++) {
940
+                    $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value[$i], $indent);
941
+                }
942
+
943
+            // Only one header of this sort
944
+            } else {
945
+                $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value, $indent);
946
+            }
947
+        }
948
+
949
+        if (!empty($input->parts)) {
950
+            for ($i = 0; $i < count($input->parts); $i++) {
951
+                $output .= $crlf . str_repeat($htab, $indent) . '<mimepart>' . $crlf .
952
+                           Mail_mimeDecode::_getXML($input->parts[$i], $indent+1) .
953
+                           str_repeat($htab, $indent) . '</mimepart>' . $crlf;
954
+            }
955
+        } elseif (isset($input->body)) {
956
+            $output .= $crlf . str_repeat($htab, $indent) . '<body><![CDATA[' .
957
+                       $input->body . ']]></body>' . $crlf;
958
+        }
959
+
960
+        return $output;
961
+    }
962
+
963
+    /**
964
+     * Helper function to _getXML(). Returns xml of a header.
965
+     *
966
+     * @param  string  Name of header
967
+     * @param  string  Value of header
968
+     * @param  integer Number of tabs to indent
969
+     * @return string  XML version of input
970
+     * @access private
971
+     */
972
+    function _getXML_helper($hdr_name, $hdr_value, $indent)
973
+    {
974
+        $htab   = "\t";
975
+        $crlf   = "\r\n";
976
+        $return = '';
977
+
978
+        $new_hdr_value = ($hdr_name != 'received') ? Mail_mimeDecode::_parseHeaderValue($hdr_value) : array('value' => $hdr_value);
979
+        $new_hdr_name  = str_replace(' ', '-', ucwords(str_replace('-', ' ', $hdr_name)));
980
+
981
+        // Sort out any parameters
982
+        if (!empty($new_hdr_value['other'])) {
983
+            foreach ($new_hdr_value['other'] as $paramname => $paramvalue) {
984
+                $params[] = str_repeat($htab, $indent) . $htab . '<parameter>' . $crlf .
985
+                            str_repeat($htab, $indent) . $htab . $htab . '<paramname>' . htmlspecialchars($paramname) . '</paramname>' . $crlf .
986
+                            str_repeat($htab, $indent) . $htab . $htab . '<paramvalue>' . htmlspecialchars($paramvalue) . '</paramvalue>' . $crlf .
987
+                            str_repeat($htab, $indent) . $htab . '</parameter>' . $crlf;
988
+            }
989
+
990
+            $params = implode('', $params);
991
+        } else {
992
+            $params = '';
993
+        }
994
+
995
+        $return = str_repeat($htab, $indent) . '<header>' . $crlf .
996
+                  str_repeat($htab, $indent) . $htab . '<headername>' . htmlspecialchars($new_hdr_name) . '</headername>' . $crlf .
997
+                  str_repeat($htab, $indent) . $htab . '<headervalue>' . htmlspecialchars($new_hdr_value['value']) . '</headervalue>' . $crlf .
998
+                  $params .
999
+                  str_repeat($htab, $indent) . '</header>' . $crlf;
1000
+
1001
+        return $return;
1002
+    }
1003
+
1004
+} // End of class
1005
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Mail/mimePart.php Added
1230
 
1
@@ -0,0 +1,1228 @@
2
+<?php
3
+/**
4
+ * The Mail_mimePart class is used to create MIME E-mail messages
5
+ *
6
+ * This class enables you to manipulate and build a mime email
7
+ * from the ground up. The Mail_Mime class is a userfriendly api
8
+ * to this class for people who aren't interested in the internals
9
+ * of mime mail.
10
+ * This class however allows full control over the email.
11
+ *
12
+ * Compatible with PHP versions 4 and 5
13
+ *
14
+ * LICENSE: This LICENSE is in the BSD license style.
15
+ * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org>
16
+ * Copyright (c) 2003-2006, PEAR <pear-group@php.net>
17
+ * All rights reserved.
18
+ *
19
+ * Redistribution and use in source and binary forms, with or
20
+ * without modification, are permitted provided that the following
21
+ * conditions are met:
22
+ *
23
+ * - Redistributions of source code must retain the above copyright
24
+ *   notice, this list of conditions and the following disclaimer.
25
+ * - Redistributions in binary form must reproduce the above copyright
26
+ *   notice, this list of conditions and the following disclaimer in the
27
+ *   documentation and/or other materials provided with the distribution.
28
+ * - Neither the name of the authors, nor the names of its contributors 
29
+ *   may be used to endorse or promote products derived from this 
30
+ *   software without specific prior written permission.
31
+ *
32
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
33
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
34
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
35
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
36
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
37
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
38
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
39
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
40
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
42
+ * THE POSSIBILITY OF SUCH DAMAGE.
43
+ *
44
+ * @category  Mail
45
+ * @package   Mail_Mime
46
+ * @author    Richard Heyes  <richard@phpguru.org>
47
+ * @author    Cipriano Groenendal <cipri@php.net>
48
+ * @author    Sean Coates <sean@php.net>
49
+ * @author    Aleksander Machniak <alec@php.net>
50
+ * @copyright 2003-2006 PEAR <pear-group@php.net>
51
+ * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
52
+ * @version   1.8.5
53
+ * @link      http://pear.php.net/package/Mail_mime
54
+ */
55
+
56
+
57
+/**
58
+ * The Mail_mimePart class is used to create MIME E-mail messages
59
+ *
60
+ * This class enables you to manipulate and build a mime email
61
+ * from the ground up. The Mail_Mime class is a userfriendly api
62
+ * to this class for people who aren't interested in the internals
63
+ * of mime mail.
64
+ * This class however allows full control over the email.
65
+ *
66
+ * @category  Mail
67
+ * @package   Mail_Mime
68
+ * @author    Richard Heyes  <richard@phpguru.org>
69
+ * @author    Cipriano Groenendal <cipri@php.net>
70
+ * @author    Sean Coates <sean@php.net>
71
+ * @author    Aleksander Machniak <alec@php.net>
72
+ * @copyright 2003-2006 PEAR <pear-group@php.net>
73
+ * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
74
+ * @version   Release: 1.8.5
75
+ * @link      http://pear.php.net/package/Mail_mime
76
+ */
77
+class Mail_mimePart
78
+{
79
+    /**
80
+    * The encoding type of this part
81
+    *
82
+    * @var string
83
+    * @access private
84
+    */
85
+    var $_encoding;
86
+
87
+    /**
88
+    * An array of subparts
89
+    *
90
+    * @var array
91
+    * @access private
92
+    */
93
+    var $_subparts;
94
+
95
+    /**
96
+    * The output of this part after being built
97
+    *
98
+    * @var string
99
+    * @access private
100
+    */
101
+    var $_encoded;
102
+
103
+    /**
104
+    * Headers for this part
105
+    *
106
+    * @var array
107
+    * @access private
108
+    */
109
+    var $_headers;
110
+
111
+    /**
112
+    * The body of this part (not encoded)
113
+    *
114
+    * @var string
115
+    * @access private
116
+    */
117
+    var $_body;
118
+
119
+    /**
120
+    * The location of file with body of this part (not encoded)
121
+    *
122
+    * @var string
123
+    * @access private
124
+    */
125
+    var $_body_file;
126
+
127
+    /**
128
+    * The end-of-line sequence
129
+    *
130
+    * @var string
131
+    * @access private
132
+    */
133
+    var $_eol = "\r\n";
134
+
135
+
136
+    /**
137
+    * Constructor.
138
+    *
139
+    * Sets up the object.
140
+    *
141
+    * @param string $body   The body of the mime part if any.
142
+    * @param array  $params An associative array of optional parameters:
143
+    *     content_type      - The content type for this part eg multipart/mixed
144
+    *     encoding          - The encoding to use, 7bit, 8bit,
145
+    *                         base64, or quoted-printable
146
+    *     charset           - Content character set
147
+    *     cid               - Content ID to apply
148
+    *     disposition       - Content disposition, inline or attachment
149
+    *     filename          - Filename parameter for content disposition
150
+    *     description       - Content description
151
+    *     name_encoding     - Encoding of the attachment name (Content-Type)
152
+    *                         By default filenames are encoded using RFC2231
153
+    *                         Here you can set RFC2047 encoding (quoted-printable
154
+    *                         or base64) instead
155
+    *     filename_encoding - Encoding of the attachment filename (Content-Disposition)
156
+    *                         See 'name_encoding'
157
+    *     headers_charset   - Charset of the headers e.g. filename, description.
158
+    *                         If not set, 'charset' will be used
159
+    *     eol               - End of line sequence. Default: "\r\n"
160
+    *     headers           - Hash array with additional part headers. Array keys can be
161
+    *                         in form of <header_name>:<parameter_name>
162
+    *     body_file         - Location of file with part's body (instead of $body)
163
+    *
164
+    * @access public
165
+    */
166
+    function Mail_mimePart($body = '', $params = array())
167
+    {
168
+        if (!empty($params['eol'])) {
169
+            $this->_eol = $params['eol'];
170
+        } else if (defined('MAIL_MIMEPART_CRLF')) { // backward-copat.
171
+            $this->_eol = MAIL_MIMEPART_CRLF;
172
+        }
173
+
174
+        // Additional part headers
175
+        if (!empty($params['headers']) && is_array($params['headers'])) {
176
+            $headers = $params['headers'];
177
+        }
178
+
179
+        foreach ($params as $key => $value) {
180
+            switch ($key) {
181
+            case 'encoding':
182
+                $this->_encoding = $value;
183
+                $headers['Content-Transfer-Encoding'] = $value;
184
+                break;
185
+
186
+            case 'cid':
187
+                $headers['Content-ID'] = '<' . $value . '>';
188
+                break;
189
+
190
+            case 'location':
191
+                $headers['Content-Location'] = $value;
192
+                break;
193
+
194
+            case 'body_file':
195
+                $this->_body_file = $value;
196
+                break;
197
+
198
+            // for backward compatibility
199
+            case 'dfilename':
200
+                $params['filename'] = $value;
201
+                break;
202
+            }
203
+        }
204
+
205
+        // Default content-type
206
+        if (empty($params['content_type'])) {
207
+            $params['content_type'] = 'text/plain';
208
+        }
209
+
210
+        // Content-Type
211
+        $headers['Content-Type'] = $params['content_type'];
212
+        if (!empty($params['charset'])) {
213
+            $charset = "charset={$params['charset']}";
214
+            // place charset parameter in the same line, if possible
215
+            if ((strlen($headers['Content-Type']) + strlen($charset) + 16) <= 76) {
216
+                $headers['Content-Type'] .= '; ';
217
+            } else {
218
+                $headers['Content-Type'] .= ';' . $this->_eol . ' ';
219
+            }
220
+            $headers['Content-Type'] .= $charset;
221
+
222
+            // Default headers charset
223
+            if (!isset($params['headers_charset'])) {
224
+                $params['headers_charset'] = $params['charset'];
225
+            }
226
+        }
227
+
228
+        // header values encoding parameters
229
+        $h_charset  = !empty($params['headers_charset']) ? $params['headers_charset'] : 'US-ASCII';
230
+        $h_language = !empty($params['language']) ? $params['language'] : null;
231
+        $h_encoding = !empty($params['name_encoding']) ? $params['name_encoding'] : null;
232
+
233
+
234
+        if (!empty($params['filename'])) {
235
+            $headers['Content-Type'] .= ';' . $this->_eol;
236
+            $headers['Content-Type'] .= $this->_buildHeaderParam(
237
+                'name', $params['filename'], $h_charset, $h_language, $h_encoding
238
+            );
239
+        }
240
+
241
+        // Content-Disposition
242
+        if (!empty($params['disposition'])) {
243
+            $headers['Content-Disposition'] = $params['disposition'];
244
+            if (!empty($params['filename'])) {
245
+                $headers['Content-Disposition'] .= ';' . $this->_eol;
246
+                $headers['Content-Disposition'] .= $this->_buildHeaderParam(
247
+                    'filename', $params['filename'], $h_charset, $h_language,
248
+                    !empty($params['filename_encoding']) ? $params['filename_encoding'] : null
249
+                );
250
+            }
251
+
252
+            // add attachment size
253
+            $size = $this->_body_file ? filesize($this->_body_file) : strlen($body);
254
+            if ($size) {
255
+                $headers['Content-Disposition'] .= ';' . $this->_eol . ' size=' . $size;
256
+            }
257
+        }
258
+
259
+        if (!empty($params['description'])) {
260
+            $headers['Content-Description'] = $this->encodeHeader(
261
+                'Content-Description', $params['description'], $h_charset, $h_encoding,
262
+                $this->_eol
263
+            );
264
+        }
265
+
266
+        // Search and add existing headers' parameters
267
+        foreach ($headers as $key => $value) {
268
+            $items = explode(':', $key);
269
+            if (count($items) == 2) {
270
+                $header = $items[0];
271
+                $param  = $items[1];
272
+                if (isset($headers[$header])) {
273
+                    $headers[$header] .= ';' . $this->_eol;
274
+                }
275
+                $headers[$header] .= $this->_buildHeaderParam(
276
+                    $param, $value, $h_charset, $h_language, $h_encoding
277
+                );
278
+                unset($headers[$key]);
279
+            }
280
+        }
281
+
282
+        // Default encoding
283
+        if (!isset($this->_encoding)) {
284
+            $this->_encoding = '7bit';
285
+        }
286
+
287
+        // Assign stuff to member variables
288
+        $this->_encoded  = array();
289
+        $this->_headers  = $headers;
290
+        $this->_body     = $body;
291
+    }
292
+
293
+    /**
294
+     * Encodes and returns the email. Also stores
295
+     * it in the encoded member variable
296
+     *
297
+     * @param string $boundary Pre-defined boundary string
298
+     *
299
+     * @return An associative array containing two elements,
300
+     *         body and headers. The headers element is itself
301
+     *         an indexed array. On error returns PEAR error object.
302
+     * @access public
303
+     */
304
+    function encode($boundary=null)
305
+    {
306
+        $encoded =& $this->_encoded;
307
+
308
+        if (count($this->_subparts)) {
309
+            $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime());
310
+            $eol = $this->_eol;
311
+
312
+            $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\"";
313
+
314
+            $encoded['body'] = ''; 
315
+
316
+            for ($i = 0; $i < count($this->_subparts); $i++) {
317
+                $encoded['body'] .= '--' . $boundary . $eol;
318
+                $tmp = $this->_subparts[$i]->encode();
319
+                if (PEAR::isError($tmp)) {
320
+                    return $tmp;
321
+                }
322
+                foreach ($tmp['headers'] as $key => $value) {
323
+                    $encoded['body'] .= $key . ': ' . $value . $eol;
324
+                }
325
+                $encoded['body'] .= $eol . $tmp['body'] . $eol;
326
+            }
327
+
328
+            $encoded['body'] .= '--' . $boundary . '--' . $eol;
329
+
330
+        } else if ($this->_body) {
331
+            $encoded['body'] = $this->_getEncodedData($this->_body, $this->_encoding);
332
+        } else if ($this->_body_file) {
333
+            // Temporarily reset magic_quotes_runtime for file reads and writes
334
+            if ($magic_quote_setting = get_magic_quotes_runtime()) {
335
+                @ini_set('magic_quotes_runtime', 0);
336
+            }
337
+            $body = $this->_getEncodedDataFromFile($this->_body_file, $this->_encoding);
338
+            if ($magic_quote_setting) {
339
+                @ini_set('magic_quotes_runtime', $magic_quote_setting);
340
+            }
341
+
342
+            if (PEAR::isError($body)) {
343
+                return $body;
344
+            }
345
+            $encoded['body'] = $body;
346
+        } else {
347
+            $encoded['body'] = '';
348
+        }
349
+
350
+        // Add headers to $encoded
351
+        $encoded['headers'] =& $this->_headers;
352
+
353
+        return $encoded;
354
+    }
355
+
356
+    /**
357
+     * Encodes and saves the email into file. File must exist.
358
+     * Data will be appended to the file.
359
+     *
360
+     * @param string  $filename  Output file location
361
+     * @param string  $boundary  Pre-defined boundary string
362
+     * @param boolean $skip_head True if you don't want to save headers
363
+     *
364
+     * @return array An associative array containing message headers
365
+     *               or PEAR error object
366
+     * @access public
367
+     * @since 1.6.0
368
+     */
369
+    function encodeToFile($filename, $boundary=null, $skip_head=false)
370
+    {
371
+        if (file_exists($filename) && !is_writable($filename)) {
372
+            $err = PEAR::raiseError('File is not writeable: ' . $filename);
373
+            return $err;
374
+        }
375
+
376
+        if (!($fh = fopen($filename, 'ab'))) {
377
+            $err = PEAR::raiseError('Unable to open file: ' . $filename);
378
+            return $err;
379
+        }
380
+
381
+        // Temporarily reset magic_quotes_runtime for file reads and writes
382
+        if ($magic_quote_setting = get_magic_quotes_runtime()) {
383
+            @ini_set('magic_quotes_runtime', 0);
384
+        }
385
+
386
+        $res = $this->_encodePartToFile($fh, $boundary, $skip_head);
387
+
388
+        fclose($fh);
389
+
390
+        if ($magic_quote_setting) {
391
+            @ini_set('magic_quotes_runtime', $magic_quote_setting);
392
+        }
393
+
394
+        return PEAR::isError($res) ? $res : $this->_headers;
395
+    }
396
+
397
+    /**
398
+     * Encodes given email part into file
399
+     *
400
+     * @param string  $fh        Output file handle
401
+     * @param string  $boundary  Pre-defined boundary string
402
+     * @param boolean $skip_head True if you don't want to save headers
403
+     *
404
+     * @return array True on sucess or PEAR error object
405
+     * @access private
406
+     */
407
+    function _encodePartToFile($fh, $boundary=null, $skip_head=false)
408
+    {
409
+        $eol = $this->_eol;
410
+
411
+        if (count($this->_subparts)) {
412
+            $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime());
413
+            $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\"";
414
+        }
415
+
416
+        if (!$skip_head) {
417
+            foreach ($this->_headers as $key => $value) {
418
+                fwrite($fh, $key . ': ' . $value . $eol);
419
+            }
420
+            $f_eol = $eol;
421
+        } else {
422
+            $f_eol = '';
423
+        }
424
+
425
+        if (count($this->_subparts)) {
426
+            for ($i = 0; $i < count($this->_subparts); $i++) {
427
+                fwrite($fh, $f_eol . '--' . $boundary . $eol);
428
+                $res = $this->_subparts[$i]->_encodePartToFile($fh);
429
+                if (PEAR::isError($res)) {
430
+                    return $res;
431
+                }
432
+                $f_eol = $eol;
433
+            }
434
+
435
+            fwrite($fh, $eol . '--' . $boundary . '--' . $eol);
436
+
437
+        } else if ($this->_body) {
438
+            fwrite($fh, $f_eol . $this->_getEncodedData($this->_body, $this->_encoding));
439
+        } else if ($this->_body_file) {
440
+            fwrite($fh, $f_eol);
441
+            $res = $this->_getEncodedDataFromFile(
442
+                $this->_body_file, $this->_encoding, $fh
443
+            );
444
+            if (PEAR::isError($res)) {
445
+                return $res;
446
+            }
447
+        }
448
+
449
+        return true;
450
+    }
451
+
452
+    /**
453
+     * Adds a subpart to current mime part and returns
454
+     * a reference to it
455
+     *
456
+     * @param string $body   The body of the subpart, if any.
457
+     * @param array  $params The parameters for the subpart, same
458
+     *                       as the $params argument for constructor.
459
+     *
460
+     * @return Mail_mimePart A reference to the part you just added. It is
461
+     *                       crucial if using multipart/* in your subparts that
462
+     *                       you use =& in your script when calling this function,
463
+     *                       otherwise you will not be able to add further subparts.
464
+     * @access public
465
+     */
466
+    function &addSubpart($body, $params)
467
+    {
468
+        $this->_subparts[] = new Mail_mimePart($body, $params);
469
+        return $this->_subparts[count($this->_subparts) - 1];
470
+    }
471
+
472
+    /**
473
+     * Returns encoded data based upon encoding passed to it
474
+     *
475
+     * @param string $data     The data to encode.
476
+     * @param string $encoding The encoding type to use, 7bit, base64,
477
+     *                         or quoted-printable.
478
+     *
479
+     * @return string
480
+     * @access private
481
+     */
482
+    function _getEncodedData($data, $encoding)
483
+    {
484
+        switch ($encoding) {
485
+        case 'quoted-printable':
486
+            return $this->_quotedPrintableEncode($data);
487
+            break;
488
+
489
+        case 'base64':
490
+            return rtrim(chunk_split(base64_encode($data), 76, $this->_eol));
491
+            break;
492
+
493
+        case '8bit':
494
+        case '7bit':
495
+        default:
496
+            return $data;
497
+        }
498
+    }
499
+
500
+    /**
501
+     * Returns encoded data based upon encoding passed to it
502
+     *
503
+     * @param string   $filename Data file location
504
+     * @param string   $encoding The encoding type to use, 7bit, base64,
505
+     *                           or quoted-printable.
506
+     * @param resource $fh       Output file handle. If set, data will be
507
+     *                           stored into it instead of returning it
508
+     *
509
+     * @return string Encoded data or PEAR error object
510
+     * @access private
511
+     */
512
+    function _getEncodedDataFromFile($filename, $encoding, $fh=null)
513
+    {
514
+        if (!is_readable($filename)) {
515
+            $err = PEAR::raiseError('Unable to read file: ' . $filename);
516
+            return $err;
517
+        }
518
+
519
+        if (!($fd = fopen($filename, 'rb'))) {
520
+            $err = PEAR::raiseError('Could not open file: ' . $filename);
521
+            return $err;
522
+        }
523
+
524
+        $data = '';
525
+
526
+        switch ($encoding) {
527
+        case 'quoted-printable':
528
+            while (!feof($fd)) {
529
+                $buffer = $this->_quotedPrintableEncode(fgets($fd));
530
+                if ($fh) {
531
+                    fwrite($fh, $buffer);
532
+                } else {
533
+                    $data .= $buffer;
534
+                }
535
+            }
536
+            break;
537
+
538
+        case 'base64':
539
+            while (!feof($fd)) {
540
+                // Should read in a multiple of 57 bytes so that
541
+                // the output is 76 bytes per line. Don't use big chunks
542
+                // because base64 encoding is memory expensive
543
+                $buffer = fread($fd, 57 * 9198); // ca. 0.5 MB
544
+                $buffer = base64_encode($buffer);
545
+                $buffer = chunk_split($buffer, 76, $this->_eol);
546
+                if (feof($fd)) {
547
+                    $buffer = rtrim($buffer);
548
+                }
549
+
550
+                if ($fh) {
551
+                    fwrite($fh, $buffer);
552
+                } else {
553
+                    $data .= $buffer;
554
+                }
555
+            }
556
+            break;
557
+
558
+        case '8bit':
559
+        case '7bit':
560
+        default:
561
+            while (!feof($fd)) {
562
+                $buffer = fread($fd, 1048576); // 1 MB
563
+                if ($fh) {
564
+                    fwrite($fh, $buffer);
565
+                } else {
566
+                    $data .= $buffer;
567
+                }
568
+            }
569
+        }
570
+
571
+        fclose($fd);
572
+
573
+        if (!$fh) {
574
+            return $data;
575
+        }
576
+    }
577
+
578
+    /**
579
+     * Encodes data to quoted-printable standard.
580
+     *
581
+     * @param string $input    The data to encode
582
+     * @param int    $line_max Optional max line length. Should
583
+     *                         not be more than 76 chars
584
+     *
585
+     * @return string Encoded data
586
+     *
587
+     * @access private
588
+     */
589
+    function _quotedPrintableEncode($input , $line_max = 76)
590
+    {
591
+        $eol = $this->_eol;
592
+        /*
593
+        // imap_8bit() is extremely fast, but doesn't handle properly some characters
594
+        if (function_exists('imap_8bit') && $line_max == 76) {
595
+            $input = preg_replace('/\r?\n/', "\r\n", $input);
596
+            $input = imap_8bit($input);
597
+            if ($eol != "\r\n") {
598
+                $input = str_replace("\r\n", $eol, $input);
599
+            }
600
+            return $input;
601
+        }
602
+        */
603
+        $lines  = preg_split("/\r?\n/", $input);
604
+        $escape = '=';
605
+        $output = '';
606
+
607
+        while (list($idx, $line) = each($lines)) {
608
+            $newline = '';
609
+            $i = 0;
610
+
611
+            while (isset($line[$i])) {
612
+                $char = $line[$i];
613
+                $dec  = ord($char);
614
+                $i++;
615
+
616
+                if (($dec == 32) && (!isset($line[$i]))) {
617
+                    // convert space at eol only
618
+                    $char = '=20';
619
+                } elseif ($dec == 9 && isset($line[$i])) {
620
+                    ; // Do nothing if a TAB is not on eol
621
+                } elseif (($dec == 61) || ($dec < 32) || ($dec > 126)) {
622
+                    $char = $escape . sprintf('%02X', $dec);
623
+                } elseif (($dec == 46) && (($newline == '')
624
+                    || ((strlen($newline) + strlen("=2E")) >= $line_max))
625
+                ) {
626
+                    // Bug #9722: convert full-stop at bol,
627
+                    // some Windows servers need this, won't break anything (cipri)
628
+                    // Bug #11731: full-stop at bol also needs to be encoded
629
+                    // if this line would push us over the line_max limit.
630
+                    $char = '=2E';
631
+                }
632
+
633
+                // Note, when changing this line, also change the ($dec == 46)
634
+                // check line, as it mimics this line due to Bug #11731
635
+                // EOL is not counted
636
+                if ((strlen($newline) + strlen($char)) >= $line_max) {
637
+                    // soft line break; " =\r\n" is okay
638
+                    $output  .= $newline . $escape . $eol;
639
+                    $newline  = '';
640
+                }
641
+                $newline .= $char;
642
+            } // end of for
643
+            $output .= $newline . $eol;
644
+            unset($lines[$idx]);
645
+        }
646
+        // Don't want last crlf
647
+        $output = substr($output, 0, -1 * strlen($eol));
648
+        return $output;
649
+    }
650
+
651
+    /**
652
+     * Encodes the parameter of a header.
653
+     *
654
+     * @param string $name      The name of the header-parameter
655
+     * @param string $value     The value of the paramter
656
+     * @param string $charset   The characterset of $value
657
+     * @param string $language  The language used in $value
658
+     * @param string $encoding  Parameter encoding. If not set, parameter value
659
+     *                          is encoded according to RFC2231
660
+     * @param int    $maxLength The maximum length of a line. Defauls to 75
661
+     *
662
+     * @return string
663
+     *
664
+     * @access private
665
+     */
666
+    function _buildHeaderParam($name, $value, $charset=null, $language=null,
667
+        $encoding=null, $maxLength=75
668
+    ) {
669
+        // RFC 2045:
670
+        // value needs encoding if contains non-ASCII chars or is longer than 78 chars
671
+        if (!preg_match('#[^\x20-\x7E]#', $value)) {
672
+            $token_regexp = '#([^\x21\x23-\x27\x2A\x2B\x2D'
673
+                . '\x2E\x30-\x39\x41-\x5A\x5E-\x7E])#';
674
+            if (!preg_match($token_regexp, $value)) {
675
+                // token
676
+                if (strlen($name) + strlen($value) + 3 <= $maxLength) {
677
+                    return " {$name}={$value}";
678
+                }
679
+            } else {
680
+                // quoted-string
681
+                $quoted = addcslashes($value, '\\"');
682
+                if (strlen($name) + strlen($quoted) + 5 <= $maxLength) {
683
+                    return " {$name}=\"{$quoted}\"";
684
+                }
685
+            }
686
+        }
687
+
688
+        // RFC2047: use quoted-printable/base64 encoding
689
+        if ($encoding == 'quoted-printable' || $encoding == 'base64') {
690
+            return $this->_buildRFC2047Param($name, $value, $charset, $encoding);
691
+        }
692
+
693
+        // RFC2231:
694
+        $encValue = preg_replace_callback(
695
+            '/([^\x21\x23\x24\x26\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E])/',
696
+            array($this, '_encodeReplaceCallback'), $value
697
+        );
698
+        $value = "$charset'$language'$encValue";
699
+
700
+        $header = " {$name}*={$value}";
701
+        if (strlen($header) <= $maxLength) {
702
+            return $header;
703
+        }
704
+
705
+        $preLength = strlen(" {$name}*0*=");
706
+        $maxLength = max(16, $maxLength - $preLength - 3);
707
+        $maxLengthReg = "|(.{0,$maxLength}[^\%][^\%])|";
708
+
709
+        $headers = array();
710
+        $headCount = 0;
711
+        while ($value) {
712
+            $matches = array();
713
+            $found = preg_match($maxLengthReg, $value, $matches);
714
+            if ($found) {
715
+                $headers[] = " {$name}*{$headCount}*={$matches[0]}";
716
+                $value = substr($value, strlen($matches[0]));
717
+            } else {
718
+                $headers[] = " {$name}*{$headCount}*={$value}";
719
+                $value = '';
720
+            }
721
+            $headCount++;
722
+        }
723
+
724
+        $headers = implode(';' . $this->_eol, $headers);
725
+        return $headers;
726
+    }
727
+
728
+    /**
729
+     * Encodes header parameter as per RFC2047 if needed
730
+     *
731
+     * @param string $name      The parameter name
732
+     * @param string $value     The parameter value
733
+     * @param string $charset   The parameter charset
734
+     * @param string $encoding  Encoding type (quoted-printable or base64)
735
+     * @param int    $maxLength Encoded parameter max length. Default: 76
736
+     *
737
+     * @return string Parameter line
738
+     * @access private
739
+     */
740
+    function _buildRFC2047Param($name, $value, $charset,
741
+        $encoding='quoted-printable', $maxLength=76
742
+    ) {
743
+        // WARNING: RFC 2047 says: "An 'encoded-word' MUST NOT be used in
744
+        // parameter of a MIME Content-Type or Content-Disposition field",
745
+        // but... it's supported by many clients/servers
746
+        $quoted = '';
747
+
748
+        if ($encoding == 'base64') {
749
+            $value = base64_encode($value);
750
+            $prefix = '=?' . $charset . '?B?';
751
+            $suffix = '?=';
752
+
753
+            // 2 x SPACE, 2 x '"', '=', ';'
754
+            $add_len = strlen($prefix . $suffix) + strlen($name) + 6;
755
+            $len = $add_len + strlen($value);
756
+
757
+            while ($len > $maxLength) { 
758
+                // We can cut base64-encoded string every 4 characters
759
+                $real_len = floor(($maxLength - $add_len) / 4) * 4;
760
+                $_quote = substr($value, 0, $real_len);
761
+                $value = substr($value, $real_len);
762
+
763
+                $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' ';
764
+                $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';'
765
+                $len = strlen($value) + $add_len;
766
+            }
767
+            $quoted .= $prefix . $value . $suffix;
768
+
769
+        } else {
770
+            // quoted-printable
771
+            $value = $this->encodeQP($value);
772
+            $prefix = '=?' . $charset . '?Q?';
773
+            $suffix = '?=';
774
+
775
+            // 2 x SPACE, 2 x '"', '=', ';'
776
+            $add_len = strlen($prefix . $suffix) + strlen($name) + 6;
777
+            $len = $add_len + strlen($value);
778
+
779
+            while ($len > $maxLength) {
780
+                $length = $maxLength - $add_len;
781
+                // don't break any encoded letters
782
+                if (preg_match("/^(.{0,$length}[^\=][^\=])/", $value, $matches)) {
783
+                    $_quote = $matches[1];
784
+                }
785
+
786
+                $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' ';
787
+                $value = substr($value, strlen($_quote));
788
+                $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';'
789
+                $len = strlen($value) + $add_len;
790
+            }
791
+
792
+            $quoted .= $prefix . $value . $suffix;
793
+        }
794
+
795
+        return " {$name}=\"{$quoted}\"";
796
+    }
797
+
798
+    /**
799
+     * Encodes a header as per RFC2047
800
+     *
801
+     * @param string $name     The header name
802
+     * @param string $value    The header data to encode
803
+     * @param string $charset  Character set name
804
+     * @param string $encoding Encoding name (base64 or quoted-printable)
805
+     * @param string $eol      End-of-line sequence. Default: "\r\n"
806
+     *
807
+     * @return string          Encoded header data (without a name)
808
+     * @access public
809
+     * @since 1.6.1
810
+     */
811
+    function encodeHeader($name, $value, $charset='ISO-8859-1',
812
+        $encoding='quoted-printable', $eol="\r\n"
813
+    ) {
814
+        // Structured headers
815
+        $comma_headers = array(
816
+            'from', 'to', 'cc', 'bcc', 'sender', 'reply-to',
817
+            'resent-from', 'resent-to', 'resent-cc', 'resent-bcc',
818
+            'resent-sender', 'resent-reply-to',
819
+            'return-receipt-to', 'disposition-notification-to',
820
+        );
821
+        $other_headers = array(
822
+            'references', 'in-reply-to', 'message-id', 'resent-message-id',
823
+        );
824
+
825
+        $name = strtolower($name);
826
+
827
+        if (in_array($name, $comma_headers)) {
828
+            $separator = ',';
829
+        } else if (in_array($name, $other_headers)) {
830
+            $separator = ' ';
831
+        }
832
+
833
+        if (!$charset) {
834
+            $charset = 'ISO-8859-1';
835
+        }
836
+
837
+        // Structured header (make sure addr-spec inside is not encoded)
838
+        if (!empty($separator)) {
839
+            // Simple e-mail address regexp
840
+            $email_regexp = '([^\s<]+|("[^\r\n"]+"))@\S+';
841
+
842
+            $parts = Mail_mimePart::_explodeQuotedString($separator, $value);
843
+            $value = '';
844
+
845
+            foreach ($parts as $part) {
846
+                $part = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $part);
847
+                $part = trim($part);
848
+
849
+                if (!$part) {
850
+                    continue;
851
+                }
852
+                if ($value) {
853
+                    $value .= $separator==',' ? $separator.' ' : ' ';
854
+                } else {
855
+                    $value = $name . ': ';
856
+                }
857
+
858
+                // let's find phrase (name) and/or addr-spec
859
+                if (preg_match('/^<' . $email_regexp . '>$/', $part)) {
860
+                    $value .= $part;
861
+                } else if (preg_match('/^' . $email_regexp . '$/', $part)) {
862
+                    // address without brackets and without name
863
+                    $value .= $part;
864
+                } else if (preg_match('/<*' . $email_regexp . '>*$/', $part, $matches)) {
865
+                    // address with name (handle name)
866
+                    $address = $matches[0];
867
+                    $word = str_replace($address, '', $part);
868
+                    $word = trim($word);
869
+                    // check if phrase requires quoting
870
+                    if ($word) {
871
+                        // non-ASCII: require encoding
872
+                        if (preg_match('#([\x80-\xFF]){1}#', $word)) {
873
+                            if ($word[0] == '"' && $word[strlen($word)-1] == '"') {
874
+                                // de-quote quoted-string, encoding changes
875
+                                // string to atom
876
+                                $search = array("\\\"", "\\\\");
877
+                                $replace = array("\"", "\\");
878
+                                $word = str_replace($search, $replace, $word);
879
+                                $word = substr($word, 1, -1);
880
+                            }
881
+                            // find length of last line
882
+                            if (($pos = strrpos($value, $eol)) !== false) {
883
+                                $last_len = strlen($value) - $pos;
884
+                            } else {
885
+                                $last_len = strlen($value);
886
+                            }
887
+                            $word = Mail_mimePart::encodeHeaderValue(
888
+                                $word, $charset, $encoding, $last_len, $eol
889
+                            );
890
+                        } else if (($word[0] != '"' || $word[strlen($word)-1] != '"')
891
+                            && preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $word)
892
+                        ) {
893
+                            // ASCII: quote string if needed
894
+                            $word = '"'.addcslashes($word, '\\"').'"';
895
+                        }
896
+                    }
897
+                    $value .= $word.' '.$address;
898
+                } else {
899
+                    // addr-spec not found, don't encode (?)
900
+                    $value .= $part;
901
+                }
902
+
903
+                // RFC2822 recommends 78 characters limit, use 76 from RFC2047
904
+                $value = wordwrap($value, 76, $eol . ' ');
905
+            }
906
+
907
+            // remove header name prefix (there could be EOL too)
908
+            $value = preg_replace(
909
+                '/^'.$name.':('.preg_quote($eol, '/').')* /', '', $value
910
+            );
911
+
912
+        } else {
913
+            // Unstructured header
914
+            // non-ASCII: require encoding
915
+            if (preg_match('#([\x80-\xFF]){1}#', $value)) {
916
+                if ($value[0] == '"' && $value[strlen($value)-1] == '"') {
917
+                    // de-quote quoted-string, encoding changes
918
+                    // string to atom
919
+                    $search = array("\\\"", "\\\\");
920
+                    $replace = array("\"", "\\");
921
+                    $value = str_replace($search, $replace, $value);
922
+                    $value = substr($value, 1, -1);
923
+                }
924
+                $value = Mail_mimePart::encodeHeaderValue(
925
+                    $value, $charset, $encoding, strlen($name) + 2, $eol
926
+                );
927
+            } else if (strlen($name.': '.$value) > 78) {
928
+                // ASCII: check if header line isn't too long and use folding
929
+                $value = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $value);
930
+                $tmp = wordwrap($name.': '.$value, 78, $eol . ' ');
931
+                $value = preg_replace('/^'.$name.':\s*/', '', $tmp);
932
+                // hard limit 998 (RFC2822)
933
+                $value = wordwrap($value, 998, $eol . ' ', true);
934
+            }
935
+        }
936
+
937
+        return $value;
938
+    }
939
+
940
+    /**
941
+     * Explode quoted string
942
+     *
943
+     * @param string $delimiter Delimiter expression string for preg_match()
944
+     * @param string $string    Input string
945
+     *
946
+     * @return array            String tokens array
947
+     * @access private
948
+     */
949
+    function _explodeQuotedString($delimiter, $string)
950
+    {
951
+        $result = array();
952
+        $strlen = strlen($string);
953
+
954
+        for ($q=$p=$i=0; $i < $strlen; $i++) {
955
+            if ($string[$i] == "\""
956
+                && (empty($string[$i-1]) || $string[$i-1] != "\\")
957
+            ) {
958
+                $q = $q ? false : true;
959
+            } else if (!$q && preg_match("/$delimiter/", $string[$i])) {
960
+                $result[] = substr($string, $p, $i - $p);
961
+                $p = $i + 1;
962
+            }
963
+        }
964
+
965
+        $result[] = substr($string, $p);
966
+        return $result;
967
+    }
968
+
969
+    /**
970
+     * Encodes a header value as per RFC2047
971
+     *
972
+     * @param string $value      The header data to encode
973
+     * @param string $charset    Character set name
974
+     * @param string $encoding   Encoding name (base64 or quoted-printable)
975
+     * @param int    $prefix_len Prefix length. Default: 0
976
+     * @param string $eol        End-of-line sequence. Default: "\r\n"
977
+     *
978
+     * @return string            Encoded header data
979
+     * @access public
980
+     * @since 1.6.1
981
+     */
982
+    function encodeHeaderValue($value, $charset, $encoding, $prefix_len=0, $eol="\r\n")
983
+    {
984
+        // #17311: Use multibyte aware method (requires mbstring extension)
985
+        if ($result = Mail_mimePart::encodeMB($value, $charset, $encoding, $prefix_len, $eol)) {
986
+            return $result;
987
+        }
988
+
989
+        // Generate the header using the specified params and dynamicly
990
+        // determine the maximum length of such strings.
991
+        // 75 is the value specified in the RFC.
992
+        $encoding = $encoding == 'base64' ? 'B' : 'Q';
993
+        $prefix = '=?' . $charset . '?' . $encoding .'?';
994
+        $suffix = '?=';
995
+        $maxLength = 75 - strlen($prefix . $suffix);
996
+        $maxLength1stLine = $maxLength - $prefix_len;
997
+
998
+        if ($encoding == 'B') {
999
+            // Base64 encode the entire string
1000
+            $value = base64_encode($value);
1001
+
1002
+            // We can cut base64 every 4 characters, so the real max
1003
+            // we can get must be rounded down.
1004
+            $maxLength = $maxLength - ($maxLength % 4);
1005
+            $maxLength1stLine = $maxLength1stLine - ($maxLength1stLine % 4);
1006
+
1007
+            $cutpoint = $maxLength1stLine;
1008
+            $output = '';
1009
+
1010
+            while ($value) {
1011
+                // Split translated string at every $maxLength
1012
+                $part = substr($value, 0, $cutpoint);
1013
+                $value = substr($value, $cutpoint);
1014
+                $cutpoint = $maxLength;
1015
+                // RFC 2047 specifies that any split header should
1016
+                // be seperated by a CRLF SPACE.
1017
+                if ($output) {
1018
+                    $output .= $eol . ' ';
1019
+                }
1020
+                $output .= $prefix . $part . $suffix;
1021
+            }
1022
+            $value = $output;
1023
+        } else {
1024
+            // quoted-printable encoding has been selected
1025
+            $value = Mail_mimePart::encodeQP($value);
1026
+
1027
+            // This regexp will break QP-encoded text at every $maxLength
1028
+            // but will not break any encoded letters.
1029
+            $reg1st = "|(.{0,$maxLength1stLine}[^\=][^\=])|";
1030
+            $reg2nd = "|(.{0,$maxLength}[^\=][^\=])|";
1031
+
1032
+            if (strlen($value) > $maxLength1stLine) {
1033
+                // Begin with the regexp for the first line.
1034
+                $reg = $reg1st;
1035
+                $output = '';
1036
+                while ($value) {
1037
+                    // Split translated string at every $maxLength
1038
+                    // But make sure not to break any translated chars.
1039
+                    $found = preg_match($reg, $value, $matches);
1040
+
1041
+                    // After this first line, we need to use a different
1042
+                    // regexp for the first line.
1043
+                    $reg = $reg2nd;
1044
+
1045
+                    // Save the found part and encapsulate it in the
1046
+                    // prefix & suffix. Then remove the part from the
1047
+                    // $value_out variable.
1048
+                    if ($found) {
1049
+                        $part = $matches[0];
1050
+                        $len = strlen($matches[0]);
1051
+                        $value = substr($value, $len);
1052
+                    } else {
1053
+                        $part = $value;
1054
+                        $value = '';
1055
+                    }
1056
+
1057
+                    // RFC 2047 specifies that any split header should
1058
+                    // be seperated by a CRLF SPACE
1059
+                    if ($output) {
1060
+                        $output .= $eol . ' ';
1061
+                    }
1062
+                    $output .= $prefix . $part . $suffix;
1063
+                }
1064
+                $value = $output;
1065
+            } else {
1066
+                $value = $prefix . $value . $suffix;
1067
+            }
1068
+        }
1069
+
1070
+        return $value;
1071
+    }
1072
+
1073
+    /**
1074
+     * Encodes the given string using quoted-printable
1075
+     *
1076
+     * @param string $str String to encode
1077
+     *
1078
+     * @return string     Encoded string
1079
+     * @access public
1080
+     * @since 1.6.0
1081
+     */
1082
+    function encodeQP($str)
1083
+    {
1084
+        // Bug #17226 RFC 2047 restricts some characters
1085
+        // if the word is inside a phrase, permitted chars are only:
1086
+        // ASCII letters, decimal digits, "!", "*", "+", "-", "/", "=", and "_"
1087
+
1088
+        // "=",  "_",  "?" must be encoded
1089
+        $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/';
1090
+        $str = preg_replace_callback(
1091
+            $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $str
1092
+        );
1093
+
1094
+        return str_replace(' ', '_', $str);
1095
+    }
1096
+
1097
+    /**
1098
+     * Encodes the given string using base64 or quoted-printable.
1099
+     * This method makes sure that encoded-word represents an integral
1100
+     * number of characters as per RFC2047.
1101
+     *
1102
+     * @param string $str        String to encode
1103
+     * @param string $charset    Character set name
1104
+     * @param string $encoding   Encoding name (base64 or quoted-printable)
1105
+     * @param int    $prefix_len Prefix length. Default: 0
1106
+     * @param string $eol        End-of-line sequence. Default: "\r\n"
1107
+     *
1108
+     * @return string     Encoded string
1109
+     * @access public
1110
+     * @since 1.8.0
1111
+     */
1112
+    function encodeMB($str, $charset, $encoding, $prefix_len=0, $eol="\r\n")
1113
+    {
1114
+        if (!function_exists('mb_substr') || !function_exists('mb_strlen')) {
1115
+            return;
1116
+        }
1117
+
1118
+        $encoding = $encoding == 'base64' ? 'B' : 'Q';
1119
+        // 75 is the value specified in the RFC
1120
+        $prefix = '=?' . $charset . '?'.$encoding.'?';
1121
+        $suffix = '?=';
1122
+        $maxLength = 75 - strlen($prefix . $suffix);
1123
+
1124
+        // A multi-octet character may not be split across adjacent encoded-words
1125
+        // So, we'll loop over each character
1126
+        // mb_stlen() with wrong charset will generate a warning here and return null
1127
+        $length      = mb_strlen($str, $charset);
1128
+        $result      = '';
1129
+        $line_length = $prefix_len;
1130
+
1131
+        if ($encoding == 'B') {
1132
+            // base64
1133
+            $start = 0;
1134
+            $prev  = '';
1135
+
1136
+            for ($i=1; $i<=$length; $i++) {
1137
+                // See #17311
1138
+                $chunk = mb_substr($str, $start, $i-$start, $charset);
1139
+                $chunk = base64_encode($chunk);
1140
+                $chunk_len = strlen($chunk);
1141
+
1142
+                if ($line_length + $chunk_len == $maxLength || $i == $length) {
1143
+                    if ($result) {
1144
+                        $result .= "\n";
1145
+                    }
1146
+                    $result .= $chunk;
1147
+                    $line_length = 0;
1148
+                    $start = $i;
1149
+                } else if ($line_length + $chunk_len > $maxLength) {
1150
+                    if ($result) {
1151
+                        $result .= "\n";
1152
+                    }
1153
+                    if ($prev) {
1154
+                        $result .= $prev;
1155
+                    }
1156
+                    $line_length = 0;
1157
+                    $start = $i - 1;
1158
+                } else {
1159
+                    $prev = $chunk;
1160
+                }
1161
+            }
1162
+        } else {
1163
+            // quoted-printable
1164
+            // see encodeQP()
1165
+            $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/';
1166
+
1167
+            for ($i=0; $i<=$length; $i++) {
1168
+                $char = mb_substr($str, $i, 1, $charset);
1169
+                // RFC recommends underline (instead of =20) in place of the space
1170
+                // that's one of the reasons why we're not using iconv_mime_encode()
1171
+                if ($char == ' ') {
1172
+                    $char = '_';
1173
+                    $char_len = 1;
1174
+                } else {
1175
+                    $char = preg_replace_callback(
1176
+                        $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $char
1177
+                    );
1178
+                    $char_len = strlen($char);
1179
+                }
1180
+
1181
+                if ($line_length + $char_len > $maxLength) {
1182
+                    if ($result) {
1183
+                        $result .= "\n";
1184
+                    }
1185
+                    $line_length = 0;
1186
+                }
1187
+
1188
+                $result      .= $char;
1189
+                $line_length += $char_len;
1190
+            }
1191
+        }
1192
+
1193
+        if ($result) {
1194
+            $result = $prefix
1195
+                .str_replace("\n", $suffix.$eol.' '.$prefix, $result).$suffix;
1196
+        }
1197
+
1198
+        return $result;
1199
+    }
1200
+
1201
+    /**
1202
+     * Callback function to replace extended characters (\x80-xFF) with their
1203
+     * ASCII values (RFC2047: quoted-printable)
1204
+     *
1205
+     * @param array $matches Preg_replace's matches array
1206
+     *
1207
+     * @return string        Encoded character string
1208
+     * @access private
1209
+     */
1210
+    function _qpReplaceCallback($matches)
1211
+    {
1212
+        return sprintf('=%02X', ord($matches[1]));
1213
+    }
1214
+
1215
+    /**
1216
+     * Callback function to replace extended characters (\x80-xFF) with their
1217
+     * ASCII values (RFC2231)
1218
+     *
1219
+     * @param array $matches Preg_replace's matches array
1220
+     *
1221
+     * @return string        Encoded character string
1222
+     * @access private
1223
+     */
1224
+    function _encodeReplaceCallback($matches)
1225
+    {
1226
+        return sprintf('%%%02X', ord($matches[1]));
1227
+    }
1228
+
1229
+} // End of class
1230
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Net Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Net/IDNA2 Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Net/IDNA2.php Added
3404
 
1
@@ -0,0 +1,3402 @@
2
+<?php
3
+
4
+// {{{ license
5
+
6
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
7
+//
8
+// +----------------------------------------------------------------------+
9
+// | This library is free software; you can redistribute it and/or modify |
10
+// | it under the terms of the GNU Lesser General Public License as       |
11
+// | published by the Free Software Foundation; either version 2.1 of the |
12
+// | License, or (at your option) any later version.                      |
13
+// |                                                                      |
14
+// | This library is distributed in the hope that it will be useful, but  |
15
+// | WITHOUT ANY WARRANTY; without even the implied warranty of           |
16
+// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU    |
17
+// | Lesser General Public License for more details.                      |
18
+// |                                                                      |
19
+// | You should have received a copy of the GNU Lesser General Public     |
20
+// | License along with this library; if not, write to the Free Software  |
21
+// | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 |
22
+// | USA.                                                                 |
23
+// +----------------------------------------------------------------------+
24
+//
25
+
26
+// }}}
27
+require_once 'Net/IDNA2/Exception.php';
28
+require_once 'Net/IDNA2/Exception/Nameprep.php';
29
+
30
+/**
31
+ * Encode/decode Internationalized Domain Names.
32
+ *
33
+ * The class allows to convert internationalized domain names
34
+ * (see RFC 3490 for details) as they can be used with various registries worldwide
35
+ * to be translated between their original (localized) form and their encoded form
36
+ * as it will be used in the DNS (Domain Name System).
37
+ *
38
+ * The class provides two public methods, encode() and decode(), which do exactly
39
+ * what you would expect them to do. You are allowed to use complete domain names,
40
+ * simple strings and complete email addresses as well. That means, that you might
41
+ * use any of the following notations:
42
+ *
43
+ * - www.n�rgler.com
44
+ * - xn--nrgler-wxa
45
+ * - xn--brse-5qa.xn--knrz-1ra.info
46
+ *
47
+ * Unicode input might be given as either UTF-8 string, UCS-4 string or UCS-4
48
+ * array. Unicode output is available in the same formats.
49
+ * You can select your preferred format via {@link set_paramter()}.
50
+ *
51
+ * ACE input and output is always expected to be ASCII.
52
+ *
53
+ * @package Net
54
+ * @author  Markus Nix <mnix@docuverse.de>
55
+ * @author  Matthias Sommerfeld <mso@phlylabs.de>
56
+ * @author  Stefan Neufeind <pear.neufeind@speedpartner.de>
57
+ * @version $Id: IDNA2.php 305344 2010-11-14 23:52:42Z neufeind $
58
+ */
59
+class Net_IDNA2
60
+{
61
+    // {{{ npdata
62
+    /**
63
+     * These Unicode codepoints are
64
+     * mapped to nothing, See RFC3454 for details
65
+     *
66
+     * @static
67
+     * @var array
68
+     * @access private
69
+     */
70
+    private static $_np_map_nothing = array(
71
+        0xAD,
72
+        0x34F,
73
+        0x1806,
74
+        0x180B,
75
+        0x180C,
76
+        0x180D,
77
+        0x200B,
78
+        0x200C,
79
+        0x200D,
80
+        0x2060,
81
+        0xFE00,
82
+        0xFE01,
83
+        0xFE02,
84
+        0xFE03,
85
+        0xFE04,
86
+        0xFE05,
87
+        0xFE06,
88
+        0xFE07,
89
+        0xFE08,
90
+        0xFE09,
91
+        0xFE0A,
92
+        0xFE0B,
93
+        0xFE0C,
94
+        0xFE0D,
95
+        0xFE0E,
96
+        0xFE0F,
97
+        0xFEFF
98
+    );
99
+
100
+    /**
101
+     * Prohibited codepints
102
+     *
103
+     * @static
104
+     * @var array
105
+     * @access private
106
+     */
107
+    private static $_general_prohibited = array(
108
+        0,
109
+        1,
110
+        2,
111
+        3,
112
+        4,
113
+        5,
114
+        6,
115
+        7,
116
+        8,
117
+        9,
118
+        0xA,
119
+        0xB,
120
+        0xC,
121
+        0xD,
122
+        0xE,
123
+        0xF,
124
+        0x10,
125
+        0x11,
126
+        0x12,
127
+        0x13,
128
+        0x14,
129
+        0x15,
130
+        0x16,
131
+        0x17,
132
+        0x18,
133
+        0x19,
134
+        0x1A,
135
+        0x1B,
136
+        0x1C,
137
+        0x1D,
138
+        0x1E,
139
+        0x1F,
140
+        0x20,
141
+        0x21,
142
+        0x22,
143
+        0x23,
144
+        0x24,
145
+        0x25,
146
+        0x26,
147
+        0x27,
148
+        0x28,
149
+        0x29,
150
+        0x2A,
151
+        0x2B,
152
+        0x2C,
153
+        0x2F,
154
+        0x3B,
155
+        0x3C,
156
+        0x3D,
157
+        0x3E,
158
+        0x3F,
159
+        0x40,
160
+        0x5B,
161
+        0x5C,
162
+        0x5D,
163
+        0x5E,
164
+        0x5F,
165
+        0x60,
166
+        0x7B,
167
+        0x7C,
168
+        0x7D,
169
+        0x7E,
170
+        0x7F,
171
+        0x3002
172
+    );
173
+
174
+    /**
175
+     * Codepints prohibited by Nameprep
176
+     * @static
177
+     * @var array
178
+     * @access private
179
+     */
180
+    private static $_np_prohibit = array(
181
+        0xA0,
182
+        0x1680,
183
+        0x2000,
184
+        0x2001,
185
+        0x2002,
186
+        0x2003,
187
+        0x2004,
188
+        0x2005,
189
+        0x2006,
190
+        0x2007,
191
+        0x2008,
192
+        0x2009,
193
+        0x200A,
194
+        0x200B,
195
+        0x202F,
196
+        0x205F,
197
+        0x3000,
198
+        0x6DD,
199
+        0x70F,
200
+        0x180E,
201
+        0x200C,
202
+        0x200D,
203
+        0x2028,
204
+        0x2029,
205
+        0xFEFF,
206
+        0xFFF9,
207
+        0xFFFA,
208
+        0xFFFB,
209
+        0xFFFC,
210
+        0xFFFE,
211
+        0xFFFF,
212
+        0x1FFFE,
213
+        0x1FFFF,
214
+        0x2FFFE,
215
+        0x2FFFF,
216
+        0x3FFFE,
217
+        0x3FFFF,
218
+        0x4FFFE,
219
+        0x4FFFF,
220
+        0x5FFFE,
221
+        0x5FFFF,
222
+        0x6FFFE,
223
+        0x6FFFF,
224
+        0x7FFFE,
225
+        0x7FFFF,
226
+        0x8FFFE,
227
+        0x8FFFF,
228
+        0x9FFFE,
229
+        0x9FFFF,
230
+        0xAFFFE,
231
+        0xAFFFF,
232
+        0xBFFFE,
233
+        0xBFFFF,
234
+        0xCFFFE,
235
+        0xCFFFF,
236
+        0xDFFFE,
237
+        0xDFFFF,
238
+        0xEFFFE,
239
+        0xEFFFF,
240
+        0xFFFFE,
241
+        0xFFFFF,
242
+        0x10FFFE,
243
+        0x10FFFF,
244
+        0xFFF9,
245
+        0xFFFA,
246
+        0xFFFB,
247
+        0xFFFC,
248
+        0xFFFD,
249
+        0x340,
250
+        0x341,
251
+        0x200E,
252
+        0x200F,
253
+        0x202A,
254
+        0x202B,
255
+        0x202C,
256
+        0x202D,
257
+        0x202E,
258
+        0x206A,
259
+        0x206B,
260
+        0x206C,
261
+        0x206D,
262
+        0x206E,
263
+        0x206F,
264
+        0xE0001
265
+    );
266
+
267
+    /**
268
+     * Codepoint ranges prohibited by nameprep
269
+     *
270
+     * @static
271
+     * @var array
272
+     * @access private
273
+     */
274
+    private static $_np_prohibit_ranges = array(
275
+        array(0x80,     0x9F    ),
276
+        array(0x2060,   0x206F  ),
277
+        array(0x1D173,  0x1D17A ),
278
+        array(0xE000,   0xF8FF  ),
279
+        array(0xF0000,  0xFFFFD ),
280
+        array(0x100000, 0x10FFFD),
281
+        array(0xFDD0,   0xFDEF  ),
282
+        array(0xD800,   0xDFFF  ),
283
+        array(0x2FF0,   0x2FFB  ),
284
+        array(0xE0020,  0xE007F )
285
+    );
286
+
287
+    /**
288
+     * Replacement mappings (casemapping, replacement sequences, ...)
289
+     *
290
+     * @static
291
+     * @var array
292
+     * @access private
293
+     */
294
+    private static $_np_replacemaps = array(
295
+        0x41    => array(0x61),
296
+        0x42    => array(0x62),
297
+        0x43    => array(0x63),
298
+        0x44    => array(0x64),
299
+        0x45    => array(0x65),
300
+        0x46    => array(0x66),
301
+        0x47    => array(0x67),
302
+        0x48    => array(0x68),
303
+        0x49    => array(0x69),
304
+        0x4A    => array(0x6A),
305
+        0x4B    => array(0x6B),
306
+        0x4C    => array(0x6C),
307
+        0x4D    => array(0x6D),
308
+        0x4E    => array(0x6E),
309
+        0x4F    => array(0x6F),
310
+        0x50    => array(0x70),
311
+        0x51    => array(0x71),
312
+        0x52    => array(0x72),
313
+        0x53    => array(0x73),
314
+        0x54    => array(0x74),
315
+        0x55    => array(0x75),
316
+        0x56    => array(0x76),
317
+        0x57    => array(0x77),
318
+        0x58    => array(0x78),
319
+        0x59    => array(0x79),
320
+        0x5A    => array(0x7A),
321
+        0xB5    => array(0x3BC),
322
+        0xC0    => array(0xE0),
323
+        0xC1    => array(0xE1),
324
+        0xC2    => array(0xE2),
325
+        0xC3    => array(0xE3),
326
+        0xC4    => array(0xE4),
327
+        0xC5    => array(0xE5),
328
+        0xC6    => array(0xE6),
329
+        0xC7    => array(0xE7),
330
+        0xC8    => array(0xE8),
331
+        0xC9    => array(0xE9),
332
+        0xCA    => array(0xEA),
333
+        0xCB    => array(0xEB),
334
+        0xCC    => array(0xEC),
335
+        0xCD    => array(0xED),
336
+        0xCE    => array(0xEE),
337
+        0xCF    => array(0xEF),
338
+        0xD0    => array(0xF0),
339
+        0xD1    => array(0xF1),
340
+        0xD2    => array(0xF2),
341
+        0xD3    => array(0xF3),
342
+        0xD4    => array(0xF4),
343
+        0xD5    => array(0xF5),
344
+        0xD6    => array(0xF6),
345
+        0xD8    => array(0xF8),
346
+        0xD9    => array(0xF9),
347
+        0xDA    => array(0xFA),
348
+        0xDB    => array(0xFB),
349
+        0xDC    => array(0xFC),
350
+        0xDD    => array(0xFD),
351
+        0xDE    => array(0xFE),
352
+        0xDF    => array(0x73, 0x73),
353
+        0x100   => array(0x101),
354
+        0x102   => array(0x103),
355
+        0x104   => array(0x105),
356
+        0x106   => array(0x107),
357
+        0x108   => array(0x109),
358
+        0x10A   => array(0x10B),
359
+        0x10C   => array(0x10D),
360
+        0x10E   => array(0x10F),
361
+        0x110   => array(0x111),
362
+        0x112   => array(0x113),
363
+        0x114   => array(0x115),
364
+        0x116   => array(0x117),
365
+        0x118   => array(0x119),
366
+        0x11A   => array(0x11B),
367
+        0x11C   => array(0x11D),
368
+        0x11E   => array(0x11F),
369
+        0x120   => array(0x121),
370
+        0x122   => array(0x123),
371
+        0x124   => array(0x125),
372
+        0x126   => array(0x127),
373
+        0x128   => array(0x129),
374
+        0x12A   => array(0x12B),
375
+        0x12C   => array(0x12D),
376
+        0x12E   => array(0x12F),
377
+        0x130   => array(0x69, 0x307),
378
+        0x132   => array(0x133),
379
+        0x134   => array(0x135),
380
+        0x136   => array(0x137),
381
+        0x139   => array(0x13A),
382
+        0x13B   => array(0x13C),
383
+        0x13D   => array(0x13E),
384
+        0x13F   => array(0x140),
385
+        0x141   => array(0x142),
386
+        0x143   => array(0x144),
387
+        0x145   => array(0x146),
388
+        0x147   => array(0x148),
389
+        0x149   => array(0x2BC, 0x6E),
390
+        0x14A   => array(0x14B),
391
+        0x14C   => array(0x14D),
392
+        0x14E   => array(0x14F),
393
+        0x150   => array(0x151),
394
+        0x152   => array(0x153),
395
+        0x154   => array(0x155),
396
+        0x156   => array(0x157),
397
+        0x158   => array(0x159),
398
+        0x15A   => array(0x15B),
399
+        0x15C   => array(0x15D),
400
+        0x15E   => array(0x15F),
401
+        0x160   => array(0x161),
402
+        0x162   => array(0x163),
403
+        0x164   => array(0x165),
404
+        0x166   => array(0x167),
405
+        0x168   => array(0x169),
406
+        0x16A   => array(0x16B),
407
+        0x16C   => array(0x16D),
408
+        0x16E   => array(0x16F),
409
+        0x170   => array(0x171),
410
+        0x172   => array(0x173),
411
+        0x174   => array(0x175),
412
+        0x176   => array(0x177),
413
+        0x178   => array(0xFF),
414
+        0x179   => array(0x17A),
415
+        0x17B   => array(0x17C),
416
+        0x17D   => array(0x17E),
417
+        0x17F   => array(0x73),
418
+        0x181   => array(0x253),
419
+        0x182   => array(0x183),
420
+        0x184   => array(0x185),
421
+        0x186   => array(0x254),
422
+        0x187   => array(0x188),
423
+        0x189   => array(0x256),
424
+        0x18A   => array(0x257),
425
+        0x18B   => array(0x18C),
426
+        0x18E   => array(0x1DD),
427
+        0x18F   => array(0x259),
428
+        0x190   => array(0x25B),
429
+        0x191   => array(0x192),
430
+        0x193   => array(0x260),
431
+        0x194   => array(0x263),
432
+        0x196   => array(0x269),
433
+        0x197   => array(0x268),
434
+        0x198   => array(0x199),
435
+        0x19C   => array(0x26F),
436
+        0x19D   => array(0x272),
437
+        0x19F   => array(0x275),
438
+        0x1A0   => array(0x1A1),
439
+        0x1A2   => array(0x1A3),
440
+        0x1A4   => array(0x1A5),
441
+        0x1A6   => array(0x280),
442
+        0x1A7   => array(0x1A8),
443
+        0x1A9   => array(0x283),
444
+        0x1AC   => array(0x1AD),
445
+        0x1AE   => array(0x288),
446
+        0x1AF   => array(0x1B0),
447
+        0x1B1   => array(0x28A),
448
+        0x1B2   => array(0x28B),
449
+        0x1B3   => array(0x1B4),
450
+        0x1B5   => array(0x1B6),
451
+        0x1B7   => array(0x292),
452
+        0x1B8   => array(0x1B9),
453
+        0x1BC   => array(0x1BD),
454
+        0x1C4   => array(0x1C6),
455
+        0x1C5   => array(0x1C6),
456
+        0x1C7   => array(0x1C9),
457
+        0x1C8   => array(0x1C9),
458
+        0x1CA   => array(0x1CC),
459
+        0x1CB   => array(0x1CC),
460
+        0x1CD   => array(0x1CE),
461
+        0x1CF   => array(0x1D0),
462
+        0x1D1   => array(0x1D2),
463
+        0x1D3   => array(0x1D4),
464
+        0x1D5   => array(0x1D6),
465
+        0x1D7   => array(0x1D8),
466
+        0x1D9   => array(0x1DA),
467
+        0x1DB   => array(0x1DC),
468
+        0x1DE   => array(0x1DF),
469
+        0x1E0   => array(0x1E1),
470
+        0x1E2   => array(0x1E3),
471
+        0x1E4   => array(0x1E5),
472
+        0x1E6   => array(0x1E7),
473
+        0x1E8   => array(0x1E9),
474
+        0x1EA   => array(0x1EB),
475
+        0x1EC   => array(0x1ED),
476
+        0x1EE   => array(0x1EF),
477
+        0x1F0   => array(0x6A, 0x30C),
478
+        0x1F1   => array(0x1F3),
479
+        0x1F2   => array(0x1F3),
480
+        0x1F4   => array(0x1F5),
481
+        0x1F6   => array(0x195),
482
+        0x1F7   => array(0x1BF),
483
+        0x1F8   => array(0x1F9),
484
+        0x1FA   => array(0x1FB),
485
+        0x1FC   => array(0x1FD),
486
+        0x1FE   => array(0x1FF),
487
+        0x200   => array(0x201),
488
+        0x202   => array(0x203),
489
+        0x204   => array(0x205),
490
+        0x206   => array(0x207),
491
+        0x208   => array(0x209),
492
+        0x20A   => array(0x20B),
493
+        0x20C   => array(0x20D),
494
+        0x20E   => array(0x20F),
495
+        0x210   => array(0x211),
496
+        0x212   => array(0x213),
497
+        0x214   => array(0x215),
498
+        0x216   => array(0x217),
499
+        0x218   => array(0x219),
500
+        0x21A   => array(0x21B),
501
+        0x21C   => array(0x21D),
502
+        0x21E   => array(0x21F),
503
+        0x220   => array(0x19E),
504
+        0x222   => array(0x223),
505
+        0x224   => array(0x225),
506
+        0x226   => array(0x227),
507
+        0x228   => array(0x229),
508
+        0x22A   => array(0x22B),
509
+        0x22C   => array(0x22D),
510
+        0x22E   => array(0x22F),
511
+        0x230   => array(0x231),
512
+        0x232   => array(0x233),
513
+        0x345   => array(0x3B9),
514
+        0x37A   => array(0x20, 0x3B9),
515
+        0x386   => array(0x3AC),
516
+        0x388   => array(0x3AD),
517
+        0x389   => array(0x3AE),
518
+        0x38A   => array(0x3AF),
519
+        0x38C   => array(0x3CC),
520
+        0x38E   => array(0x3CD),
521
+        0x38F   => array(0x3CE),
522
+        0x390   => array(0x3B9, 0x308, 0x301),
523
+        0x391   => array(0x3B1),
524
+        0x392   => array(0x3B2),
525
+        0x393   => array(0x3B3),
526
+        0x394   => array(0x3B4),
527
+        0x395   => array(0x3B5),
528
+        0x396   => array(0x3B6),
529
+        0x397   => array(0x3B7),
530
+        0x398   => array(0x3B8),
531
+        0x399   => array(0x3B9),
532
+        0x39A   => array(0x3BA),
533
+        0x39B   => array(0x3BB),
534
+        0x39C   => array(0x3BC),
535
+        0x39D   => array(0x3BD),
536
+        0x39E   => array(0x3BE),
537
+        0x39F   => array(0x3BF),
538
+        0x3A0   => array(0x3C0),
539
+        0x3A1   => array(0x3C1),
540
+        0x3A3   => array(0x3C3),
541
+        0x3A4   => array(0x3C4),
542
+        0x3A5   => array(0x3C5),
543
+        0x3A6   => array(0x3C6),
544
+        0x3A7   => array(0x3C7),
545
+        0x3A8   => array(0x3C8),
546
+        0x3A9   => array(0x3C9),
547
+        0x3AA   => array(0x3CA),
548
+        0x3AB   => array(0x3CB),
549
+        0x3B0   => array(0x3C5, 0x308, 0x301),
550
+        0x3C2   => array(0x3C3),
551
+        0x3D0   => array(0x3B2),
552
+        0x3D1   => array(0x3B8),
553
+        0x3D2   => array(0x3C5),
554
+        0x3D3   => array(0x3CD),
555
+        0x3D4   => array(0x3CB),
556
+        0x3D5   => array(0x3C6),
557
+        0x3D6   => array(0x3C0),
558
+        0x3D8   => array(0x3D9),
559
+        0x3DA   => array(0x3DB),
560
+        0x3DC   => array(0x3DD),
561
+        0x3DE   => array(0x3DF),
562
+        0x3E0   => array(0x3E1),
563
+        0x3E2   => array(0x3E3),
564
+        0x3E4   => array(0x3E5),
565
+        0x3E6   => array(0x3E7),
566
+        0x3E8   => array(0x3E9),
567
+        0x3EA   => array(0x3EB),
568
+        0x3EC   => array(0x3ED),
569
+        0x3EE   => array(0x3EF),
570
+        0x3F0   => array(0x3BA),
571
+        0x3F1   => array(0x3C1),
572
+        0x3F2   => array(0x3C3),
573
+        0x3F4   => array(0x3B8),
574
+        0x3F5   => array(0x3B5),
575
+        0x400   => array(0x450),
576
+        0x401   => array(0x451),
577
+        0x402   => array(0x452),
578
+        0x403   => array(0x453),
579
+        0x404   => array(0x454),
580
+        0x405   => array(0x455),
581
+        0x406   => array(0x456),
582
+        0x407   => array(0x457),
583
+        0x408   => array(0x458),
584
+        0x409   => array(0x459),
585
+        0x40A   => array(0x45A),
586
+        0x40B   => array(0x45B),
587
+        0x40C   => array(0x45C),
588
+        0x40D   => array(0x45D),
589
+        0x40E   => array(0x45E),
590
+        0x40F   => array(0x45F),
591
+        0x410   => array(0x430),
592
+        0x411   => array(0x431),
593
+        0x412   => array(0x432),
594
+        0x413   => array(0x433),
595
+        0x414   => array(0x434),
596
+        0x415   => array(0x435),
597
+        0x416   => array(0x436),
598
+        0x417   => array(0x437),
599
+        0x418   => array(0x438),
600
+        0x419   => array(0x439),
601
+        0x41A   => array(0x43A),
602
+        0x41B   => array(0x43B),
603
+        0x41C   => array(0x43C),
604
+        0x41D   => array(0x43D),
605
+        0x41E   => array(0x43E),
606
+        0x41F   => array(0x43F),
607
+        0x420   => array(0x440),
608
+        0x421   => array(0x441),
609
+        0x422   => array(0x442),
610
+        0x423   => array(0x443),
611
+        0x424   => array(0x444),
612
+        0x425   => array(0x445),
613
+        0x426   => array(0x446),
614
+        0x427   => array(0x447),
615
+        0x428   => array(0x448),
616
+        0x429   => array(0x449),
617
+        0x42A   => array(0x44A),
618
+        0x42B   => array(0x44B),
619
+        0x42C   => array(0x44C),
620
+        0x42D   => array(0x44D),
621
+        0x42E   => array(0x44E),
622
+        0x42F   => array(0x44F),
623
+        0x460   => array(0x461),
624
+        0x462   => array(0x463),
625
+        0x464   => array(0x465),
626
+        0x466   => array(0x467),
627
+        0x468   => array(0x469),
628
+        0x46A   => array(0x46B),
629
+        0x46C   => array(0x46D),
630
+        0x46E   => array(0x46F),
631
+        0x470   => array(0x471),
632
+        0x472   => array(0x473),
633
+        0x474   => array(0x475),
634
+        0x476   => array(0x477),
635
+        0x478   => array(0x479),
636
+        0x47A   => array(0x47B),
637
+        0x47C   => array(0x47D),
638
+        0x47E   => array(0x47F),
639
+        0x480   => array(0x481),
640
+        0x48A   => array(0x48B),
641
+        0x48C   => array(0x48D),
642
+        0x48E   => array(0x48F),
643
+        0x490   => array(0x491),
644
+        0x492   => array(0x493),
645
+        0x494   => array(0x495),
646
+        0x496   => array(0x497),
647
+        0x498   => array(0x499),
648
+        0x49A   => array(0x49B),
649
+        0x49C   => array(0x49D),
650
+        0x49E   => array(0x49F),
651
+        0x4A0   => array(0x4A1),
652
+        0x4A2   => array(0x4A3),
653
+        0x4A4   => array(0x4A5),
654
+        0x4A6   => array(0x4A7),
655
+        0x4A8   => array(0x4A9),
656
+        0x4AA   => array(0x4AB),
657
+        0x4AC   => array(0x4AD),
658
+        0x4AE   => array(0x4AF),
659
+        0x4B0   => array(0x4B1),
660
+        0x4B2   => array(0x4B3),
661
+        0x4B4   => array(0x4B5),
662
+        0x4B6   => array(0x4B7),
663
+        0x4B8   => array(0x4B9),
664
+        0x4BA   => array(0x4BB),
665
+        0x4BC   => array(0x4BD),
666
+        0x4BE   => array(0x4BF),
667
+        0x4C1   => array(0x4C2),
668
+        0x4C3   => array(0x4C4),
669
+        0x4C5   => array(0x4C6),
670
+        0x4C7   => array(0x4C8),
671
+        0x4C9   => array(0x4CA),
672
+        0x4CB   => array(0x4CC),
673
+        0x4CD   => array(0x4CE),
674
+        0x4D0   => array(0x4D1),
675
+        0x4D2   => array(0x4D3),
676
+        0x4D4   => array(0x4D5),
677
+        0x4D6   => array(0x4D7),
678
+        0x4D8   => array(0x4D9),
679
+        0x4DA   => array(0x4DB),
680
+        0x4DC   => array(0x4DD),
681
+        0x4DE   => array(0x4DF),
682
+        0x4E0   => array(0x4E1),
683
+        0x4E2   => array(0x4E3),
684
+        0x4E4   => array(0x4E5),
685
+        0x4E6   => array(0x4E7),
686
+        0x4E8   => array(0x4E9),
687
+        0x4EA   => array(0x4EB),
688
+        0x4EC   => array(0x4ED),
689
+        0x4EE   => array(0x4EF),
690
+        0x4F0   => array(0x4F1),
691
+        0x4F2   => array(0x4F3),
692
+        0x4F4   => array(0x4F5),
693
+        0x4F8   => array(0x4F9),
694
+        0x500   => array(0x501),
695
+        0x502   => array(0x503),
696
+        0x504   => array(0x505),
697
+        0x506   => array(0x507),
698
+        0x508   => array(0x509),
699
+        0x50A   => array(0x50B),
700
+        0x50C   => array(0x50D),
701
+        0x50E   => array(0x50F),
702
+        0x531   => array(0x561),
703
+        0x532   => array(0x562),
704
+        0x533   => array(0x563),
705
+        0x534   => array(0x564),
706
+        0x535   => array(0x565),
707
+        0x536   => array(0x566),
708
+        0x537   => array(0x567),
709
+        0x538   => array(0x568),
710
+        0x539   => array(0x569),
711
+        0x53A   => array(0x56A),
712
+        0x53B   => array(0x56B),
713
+        0x53C   => array(0x56C),
714
+        0x53D   => array(0x56D),
715
+        0x53E   => array(0x56E),
716
+        0x53F   => array(0x56F),
717
+        0x540   => array(0x570),
718
+        0x541   => array(0x571),
719
+        0x542   => array(0x572),
720
+        0x543   => array(0x573),
721
+        0x544   => array(0x574),
722
+        0x545   => array(0x575),
723
+        0x546   => array(0x576),
724
+        0x547   => array(0x577),
725
+        0x548   => array(0x578),
726
+        0x549   => array(0x579),
727
+        0x54A   => array(0x57A),
728
+        0x54B   => array(0x57B),
729
+        0x54C   => array(0x57C),
730
+        0x54D   => array(0x57D),
731
+        0x54E   => array(0x57E),
732
+        0x54F   => array(0x57F),
733
+        0x550   => array(0x580),
734
+        0x551   => array(0x581),
735
+        0x552   => array(0x582),
736
+        0x553   => array(0x583),
737
+        0x554   => array(0x584),
738
+        0x555   => array(0x585),
739
+        0x556   => array(0x586),
740
+        0x587   => array(0x565, 0x582),
741
+        0x1E00  => array(0x1E01),
742
+        0x1E02  => array(0x1E03),
743
+        0x1E04  => array(0x1E05),
744
+        0x1E06  => array(0x1E07),
745
+        0x1E08  => array(0x1E09),
746
+        0x1E0A  => array(0x1E0B),
747
+        0x1E0C  => array(0x1E0D),
748
+        0x1E0E  => array(0x1E0F),
749
+        0x1E10  => array(0x1E11),
750
+        0x1E12  => array(0x1E13),
751
+        0x1E14  => array(0x1E15),
752
+        0x1E16  => array(0x1E17),
753
+        0x1E18  => array(0x1E19),
754
+        0x1E1A  => array(0x1E1B),
755
+        0x1E1C  => array(0x1E1D),
756
+        0x1E1E  => array(0x1E1F),
757
+        0x1E20  => array(0x1E21),
758
+        0x1E22  => array(0x1E23),
759
+        0x1E24  => array(0x1E25),
760
+        0x1E26  => array(0x1E27),
761
+        0x1E28  => array(0x1E29),
762
+        0x1E2A  => array(0x1E2B),
763
+        0x1E2C  => array(0x1E2D),
764
+        0x1E2E  => array(0x1E2F),
765
+        0x1E30  => array(0x1E31),
766
+        0x1E32  => array(0x1E33),
767
+        0x1E34  => array(0x1E35),
768
+        0x1E36  => array(0x1E37),
769
+        0x1E38  => array(0x1E39),
770
+        0x1E3A  => array(0x1E3B),
771
+        0x1E3C  => array(0x1E3D),
772
+        0x1E3E  => array(0x1E3F),
773
+        0x1E40  => array(0x1E41),
774
+        0x1E42  => array(0x1E43),
775
+        0x1E44  => array(0x1E45),
776
+        0x1E46  => array(0x1E47),
777
+        0x1E48  => array(0x1E49),
778
+        0x1E4A  => array(0x1E4B),
779
+        0x1E4C  => array(0x1E4D),
780
+        0x1E4E  => array(0x1E4F),
781
+        0x1E50  => array(0x1E51),
782
+        0x1E52  => array(0x1E53),
783
+        0x1E54  => array(0x1E55),
784
+        0x1E56  => array(0x1E57),
785
+        0x1E58  => array(0x1E59),
786
+        0x1E5A  => array(0x1E5B),
787
+        0x1E5C  => array(0x1E5D),
788
+        0x1E5E  => array(0x1E5F),
789
+        0x1E60  => array(0x1E61),
790
+        0x1E62  => array(0x1E63),
791
+        0x1E64  => array(0x1E65),
792
+        0x1E66  => array(0x1E67),
793
+        0x1E68  => array(0x1E69),
794
+        0x1E6A  => array(0x1E6B),
795
+        0x1E6C  => array(0x1E6D),
796
+        0x1E6E  => array(0x1E6F),
797
+        0x1E70  => array(0x1E71),
798
+        0x1E72  => array(0x1E73),
799
+        0x1E74  => array(0x1E75),
800
+        0x1E76  => array(0x1E77),
801
+        0x1E78  => array(0x1E79),
802
+        0x1E7A  => array(0x1E7B),
803
+        0x1E7C  => array(0x1E7D),
804
+        0x1E7E  => array(0x1E7F),
805
+        0x1E80  => array(0x1E81),
806
+        0x1E82  => array(0x1E83),
807
+        0x1E84  => array(0x1E85),
808
+        0x1E86  => array(0x1E87),
809
+        0x1E88  => array(0x1E89),
810
+        0x1E8A  => array(0x1E8B),
811
+        0x1E8C  => array(0x1E8D),
812
+        0x1E8E  => array(0x1E8F),
813
+        0x1E90  => array(0x1E91),
814
+        0x1E92  => array(0x1E93),
815
+        0x1E94  => array(0x1E95),
816
+        0x1E96  => array(0x68, 0x331),
817
+        0x1E97  => array(0x74, 0x308),
818
+        0x1E98  => array(0x77, 0x30A),
819
+        0x1E99  => array(0x79, 0x30A),
820
+        0x1E9A  => array(0x61, 0x2BE),
821
+        0x1E9B  => array(0x1E61),
822
+        0x1EA0  => array(0x1EA1),
823
+        0x1EA2  => array(0x1EA3),
824
+        0x1EA4  => array(0x1EA5),
825
+        0x1EA6  => array(0x1EA7),
826
+        0x1EA8  => array(0x1EA9),
827
+        0x1EAA  => array(0x1EAB),
828
+        0x1EAC  => array(0x1EAD),
829
+        0x1EAE  => array(0x1EAF),
830
+        0x1EB0  => array(0x1EB1),
831
+        0x1EB2  => array(0x1EB3),
832
+        0x1EB4  => array(0x1EB5),
833
+        0x1EB6  => array(0x1EB7),
834
+        0x1EB8  => array(0x1EB9),
835
+        0x1EBA  => array(0x1EBB),
836
+        0x1EBC  => array(0x1EBD),
837
+        0x1EBE  => array(0x1EBF),
838
+        0x1EC0  => array(0x1EC1),
839
+        0x1EC2  => array(0x1EC3),
840
+        0x1EC4  => array(0x1EC5),
841
+        0x1EC6  => array(0x1EC7),
842
+        0x1EC8  => array(0x1EC9),
843
+        0x1ECA  => array(0x1ECB),
844
+        0x1ECC  => array(0x1ECD),
845
+        0x1ECE  => array(0x1ECF),
846
+        0x1ED0  => array(0x1ED1),
847
+        0x1ED2  => array(0x1ED3),
848
+        0x1ED4  => array(0x1ED5),
849
+        0x1ED6  => array(0x1ED7),
850
+        0x1ED8  => array(0x1ED9),
851
+        0x1EDA  => array(0x1EDB),
852
+        0x1EDC  => array(0x1EDD),
853
+        0x1EDE  => array(0x1EDF),
854
+        0x1EE0  => array(0x1EE1),
855
+        0x1EE2  => array(0x1EE3),
856
+        0x1EE4  => array(0x1EE5),
857
+        0x1EE6  => array(0x1EE7),
858
+        0x1EE8  => array(0x1EE9),
859
+        0x1EEA  => array(0x1EEB),
860
+        0x1EEC  => array(0x1EED),
861
+        0x1EEE  => array(0x1EEF),
862
+        0x1EF0  => array(0x1EF1),
863
+        0x1EF2  => array(0x1EF3),
864
+        0x1EF4  => array(0x1EF5),
865
+        0x1EF6  => array(0x1EF7),
866
+        0x1EF8  => array(0x1EF9),
867
+        0x1F08  => array(0x1F00),
868
+        0x1F09  => array(0x1F01),
869
+        0x1F0A  => array(0x1F02),
870
+        0x1F0B  => array(0x1F03),
871
+        0x1F0C  => array(0x1F04),
872
+        0x1F0D  => array(0x1F05),
873
+        0x1F0E  => array(0x1F06),
874
+        0x1F0F  => array(0x1F07),
875
+        0x1F18  => array(0x1F10),
876
+        0x1F19  => array(0x1F11),
877
+        0x1F1A  => array(0x1F12),
878
+        0x1F1B  => array(0x1F13),
879
+        0x1F1C  => array(0x1F14),
880
+        0x1F1D  => array(0x1F15),
881
+        0x1F28  => array(0x1F20),
882
+        0x1F29  => array(0x1F21),
883
+        0x1F2A  => array(0x1F22),
884
+        0x1F2B  => array(0x1F23),
885
+        0x1F2C  => array(0x1F24),
886
+        0x1F2D  => array(0x1F25),
887
+        0x1F2E  => array(0x1F26),
888
+        0x1F2F  => array(0x1F27),
889
+        0x1F38  => array(0x1F30),
890
+        0x1F39  => array(0x1F31),
891
+        0x1F3A  => array(0x1F32),
892
+        0x1F3B  => array(0x1F33),
893
+        0x1F3C  => array(0x1F34),
894
+        0x1F3D  => array(0x1F35),
895
+        0x1F3E  => array(0x1F36),
896
+        0x1F3F  => array(0x1F37),
897
+        0x1F48  => array(0x1F40),
898
+        0x1F49  => array(0x1F41),
899
+        0x1F4A  => array(0x1F42),
900
+        0x1F4B  => array(0x1F43),
901
+        0x1F4C  => array(0x1F44),
902
+        0x1F4D  => array(0x1F45),
903
+        0x1F50  => array(0x3C5, 0x313),
904
+        0x1F52  => array(0x3C5, 0x313, 0x300),
905
+        0x1F54  => array(0x3C5, 0x313, 0x301),
906
+        0x1F56  => array(0x3C5, 0x313, 0x342),
907
+        0x1F59  => array(0x1F51),
908
+        0x1F5B  => array(0x1F53),
909
+        0x1F5D  => array(0x1F55),
910
+        0x1F5F  => array(0x1F57),
911
+        0x1F68  => array(0x1F60),
912
+        0x1F69  => array(0x1F61),
913
+        0x1F6A  => array(0x1F62),
914
+        0x1F6B  => array(0x1F63),
915
+        0x1F6C  => array(0x1F64),
916
+        0x1F6D  => array(0x1F65),
917
+        0x1F6E  => array(0x1F66),
918
+        0x1F6F  => array(0x1F67),
919
+        0x1F80  => array(0x1F00, 0x3B9),
920
+        0x1F81  => array(0x1F01, 0x3B9),
921
+        0x1F82  => array(0x1F02, 0x3B9),
922
+        0x1F83  => array(0x1F03, 0x3B9),
923
+        0x1F84  => array(0x1F04, 0x3B9),
924
+        0x1F85  => array(0x1F05, 0x3B9),
925
+        0x1F86  => array(0x1F06, 0x3B9),
926
+        0x1F87  => array(0x1F07, 0x3B9),
927
+        0x1F88  => array(0x1F00, 0x3B9),
928
+        0x1F89  => array(0x1F01, 0x3B9),
929
+        0x1F8A  => array(0x1F02, 0x3B9),
930
+        0x1F8B  => array(0x1F03, 0x3B9),
931
+        0x1F8C  => array(0x1F04, 0x3B9),
932
+        0x1F8D  => array(0x1F05, 0x3B9),
933
+        0x1F8E  => array(0x1F06, 0x3B9),
934
+        0x1F8F  => array(0x1F07, 0x3B9),
935
+        0x1F90  => array(0x1F20, 0x3B9),
936
+        0x1F91  => array(0x1F21, 0x3B9),
937
+        0x1F92  => array(0x1F22, 0x3B9),
938
+        0x1F93  => array(0x1F23, 0x3B9),
939
+        0x1F94  => array(0x1F24, 0x3B9),
940
+        0x1F95  => array(0x1F25, 0x3B9),
941
+        0x1F96  => array(0x1F26, 0x3B9),
942
+        0x1F97  => array(0x1F27, 0x3B9),
943
+        0x1F98  => array(0x1F20, 0x3B9),
944
+        0x1F99  => array(0x1F21, 0x3B9),
945
+        0x1F9A  => array(0x1F22, 0x3B9),
946
+        0x1F9B  => array(0x1F23, 0x3B9),
947
+        0x1F9C  => array(0x1F24, 0x3B9),
948
+        0x1F9D  => array(0x1F25, 0x3B9),
949
+        0x1F9E  => array(0x1F26, 0x3B9),
950
+        0x1F9F  => array(0x1F27, 0x3B9),
951
+        0x1FA0  => array(0x1F60, 0x3B9),
952
+        0x1FA1  => array(0x1F61, 0x3B9),
953
+        0x1FA2  => array(0x1F62, 0x3B9),
954
+        0x1FA3  => array(0x1F63, 0x3B9),
955
+        0x1FA4  => array(0x1F64, 0x3B9),
956
+        0x1FA5  => array(0x1F65, 0x3B9),
957
+        0x1FA6  => array(0x1F66, 0x3B9),
958
+        0x1FA7  => array(0x1F67, 0x3B9),
959
+        0x1FA8  => array(0x1F60, 0x3B9),
960
+        0x1FA9  => array(0x1F61, 0x3B9),
961
+        0x1FAA  => array(0x1F62, 0x3B9),
962
+        0x1FAB  => array(0x1F63, 0x3B9),
963
+        0x1FAC  => array(0x1F64, 0x3B9),
964
+        0x1FAD  => array(0x1F65, 0x3B9),
965
+        0x1FAE  => array(0x1F66, 0x3B9),
966
+        0x1FAF  => array(0x1F67, 0x3B9),
967
+        0x1FB2  => array(0x1F70, 0x3B9),
968
+        0x1FB3  => array(0x3B1, 0x3B9),
969
+        0x1FB4  => array(0x3AC, 0x3B9),
970
+        0x1FB6  => array(0x3B1, 0x342),
971
+        0x1FB7  => array(0x3B1, 0x342, 0x3B9),
972
+        0x1FB8  => array(0x1FB0),
973
+        0x1FB9  => array(0x1FB1),
974
+        0x1FBA  => array(0x1F70),
975
+        0x1FBB  => array(0x1F71),
976
+        0x1FBC  => array(0x3B1, 0x3B9),
977
+        0x1FBE  => array(0x3B9),
978
+        0x1FC2  => array(0x1F74, 0x3B9),
979
+        0x1FC3  => array(0x3B7, 0x3B9),
980
+        0x1FC4  => array(0x3AE, 0x3B9),
981
+        0x1FC6  => array(0x3B7, 0x342),
982
+        0x1FC7  => array(0x3B7, 0x342, 0x3B9),
983
+        0x1FC8  => array(0x1F72),
984
+        0x1FC9  => array(0x1F73),
985
+        0x1FCA  => array(0x1F74),
986
+        0x1FCB  => array(0x1F75),
987
+        0x1FCC  => array(0x3B7, 0x3B9),
988
+        0x1FD2  => array(0x3B9, 0x308, 0x300),
989
+        0x1FD3  => array(0x3B9, 0x308, 0x301),
990
+        0x1FD6  => array(0x3B9, 0x342),
991
+        0x1FD7  => array(0x3B9, 0x308, 0x342),
992
+        0x1FD8  => array(0x1FD0),
993
+        0x1FD9  => array(0x1FD1),
994
+        0x1FDA  => array(0x1F76),
995
+        0x1FDB  => array(0x1F77),
996
+        0x1FE2  => array(0x3C5, 0x308, 0x300),
997
+        0x1FE3  => array(0x3C5, 0x308, 0x301),
998
+        0x1FE4  => array(0x3C1, 0x313),
999
+        0x1FE6  => array(0x3C5, 0x342),
1000
+        0x1FE7  => array(0x3C5, 0x308, 0x342),
1001
+        0x1FE8  => array(0x1FE0),
1002
+        0x1FE9  => array(0x1FE1),
1003
+        0x1FEA  => array(0x1F7A),
1004
+        0x1FEB  => array(0x1F7B),
1005
+        0x1FEC  => array(0x1FE5),
1006
+        0x1FF2  => array(0x1F7C, 0x3B9),
1007
+        0x1FF3  => array(0x3C9, 0x3B9),
1008
+        0x1FF4  => array(0x3CE, 0x3B9),
1009
+        0x1FF6  => array(0x3C9, 0x342),
1010
+        0x1FF7  => array(0x3C9, 0x342, 0x3B9),
1011
+        0x1FF8  => array(0x1F78),
1012
+        0x1FF9  => array(0x1F79),
1013
+        0x1FFA  => array(0x1F7C),
1014
+        0x1FFB  => array(0x1F7D),
1015
+        0x1FFC  => array(0x3C9, 0x3B9),
1016
+        0x20A8  => array(0x72, 0x73),
1017
+        0x2102  => array(0x63),
1018
+        0x2103  => array(0xB0, 0x63),
1019
+        0x2107  => array(0x25B),
1020
+        0x2109  => array(0xB0, 0x66),
1021
+        0x210B  => array(0x68),
1022
+        0x210C  => array(0x68),
1023
+        0x210D  => array(0x68),
1024
+        0x2110  => array(0x69),
1025
+        0x2111  => array(0x69),
1026
+        0x2112  => array(0x6C),
1027
+        0x2115  => array(0x6E),
1028
+        0x2116  => array(0x6E, 0x6F),
1029
+        0x2119  => array(0x70),
1030
+        0x211A  => array(0x71),
1031
+        0x211B  => array(0x72),
1032
+        0x211C  => array(0x72),
1033
+        0x211D  => array(0x72),
1034
+        0x2120  => array(0x73, 0x6D),
1035
+        0x2121  => array(0x74, 0x65, 0x6C),
1036
+        0x2122  => array(0x74, 0x6D),
1037
+        0x2124  => array(0x7A),
1038
+        0x2126  => array(0x3C9),
1039
+        0x2128  => array(0x7A),
1040
+        0x212A  => array(0x6B),
1041
+        0x212B  => array(0xE5),
1042
+        0x212C  => array(0x62),
1043
+        0x212D  => array(0x63),
1044
+        0x2130  => array(0x65),
1045
+        0x2131  => array(0x66),
1046
+        0x2133  => array(0x6D),
1047
+        0x213E  => array(0x3B3),
1048
+        0x213F  => array(0x3C0),
1049
+        0x2145  => array(0x64),
1050
+        0x2160  => array(0x2170),
1051
+        0x2161  => array(0x2171),
1052
+        0x2162  => array(0x2172),
1053
+        0x2163  => array(0x2173),
1054
+        0x2164  => array(0x2174),
1055
+        0x2165  => array(0x2175),
1056
+        0x2166  => array(0x2176),
1057
+        0x2167  => array(0x2177),
1058
+        0x2168  => array(0x2178),
1059
+        0x2169  => array(0x2179),
1060
+        0x216A  => array(0x217A),
1061
+        0x216B  => array(0x217B),
1062
+        0x216C  => array(0x217C),
1063
+        0x216D  => array(0x217D),
1064
+        0x216E  => array(0x217E),
1065
+        0x216F  => array(0x217F),
1066
+        0x24B6  => array(0x24D0),
1067
+        0x24B7  => array(0x24D1),
1068
+        0x24B8  => array(0x24D2),
1069
+        0x24B9  => array(0x24D3),
1070
+        0x24BA  => array(0x24D4),
1071
+        0x24BB  => array(0x24D5),
1072
+        0x24BC  => array(0x24D6),
1073
+        0x24BD  => array(0x24D7),
1074
+        0x24BE  => array(0x24D8),
1075
+        0x24BF  => array(0x24D9),
1076
+        0x24C0  => array(0x24DA),
1077
+        0x24C1  => array(0x24DB),
1078
+        0x24C2  => array(0x24DC),
1079
+        0x24C3  => array(0x24DD),
1080
+        0x24C4  => array(0x24DE),
1081
+        0x24C5  => array(0x24DF),
1082
+        0x24C6  => array(0x24E0),
1083
+        0x24C7  => array(0x24E1),
1084
+        0x24C8  => array(0x24E2),
1085
+        0x24C9  => array(0x24E3),
1086
+        0x24CA  => array(0x24E4),
1087
+        0x24CB  => array(0x24E5),
1088
+        0x24CC  => array(0x24E6),
1089
+        0x24CD  => array(0x24E7),
1090
+        0x24CE  => array(0x24E8),
1091
+        0x24CF  => array(0x24E9),
1092
+        0x3371  => array(0x68, 0x70, 0x61),
1093
+        0x3373  => array(0x61, 0x75),
1094
+        0x3375  => array(0x6F, 0x76),
1095
+        0x3380  => array(0x70, 0x61),
1096
+        0x3381  => array(0x6E, 0x61),
1097
+        0x3382  => array(0x3BC, 0x61),
1098
+        0x3383  => array(0x6D, 0x61),
1099
+        0x3384  => array(0x6B, 0x61),
1100
+        0x3385  => array(0x6B, 0x62),
1101
+        0x3386  => array(0x6D, 0x62),
1102
+        0x3387  => array(0x67, 0x62),
1103
+        0x338A  => array(0x70, 0x66),
1104
+        0x338B  => array(0x6E, 0x66),
1105
+        0x338C  => array(0x3BC, 0x66),
1106
+        0x3390  => array(0x68, 0x7A),
1107
+        0x3391  => array(0x6B, 0x68, 0x7A),
1108
+        0x3392  => array(0x6D, 0x68, 0x7A),
1109
+        0x3393  => array(0x67, 0x68, 0x7A),
1110
+        0x3394  => array(0x74, 0x68, 0x7A),
1111
+        0x33A9  => array(0x70, 0x61),
1112
+        0x33AA  => array(0x6B, 0x70, 0x61),
1113
+        0x33AB  => array(0x6D, 0x70, 0x61),
1114
+        0x33AC  => array(0x67, 0x70, 0x61),
1115
+        0x33B4  => array(0x70, 0x76),
1116
+        0x33B5  => array(0x6E, 0x76),
1117
+        0x33B6  => array(0x3BC, 0x76),
1118
+        0x33B7  => array(0x6D, 0x76),
1119
+        0x33B8  => array(0x6B, 0x76),
1120
+        0x33B9  => array(0x6D, 0x76),
1121
+        0x33BA  => array(0x70, 0x77),
1122
+        0x33BB  => array(0x6E, 0x77),
1123
+        0x33BC  => array(0x3BC, 0x77),
1124
+        0x33BD  => array(0x6D, 0x77),
1125
+        0x33BE  => array(0x6B, 0x77),
1126
+        0x33BF  => array(0x6D, 0x77),
1127
+        0x33C0  => array(0x6B, 0x3C9),
1128
+        0x33C1  => array(0x6D, 0x3C9),
1129
+        /* 0x33C2  => array(0x61, 0x2E, 0x6D, 0x2E), */
1130
+        0x33C3  => array(0x62, 0x71),
1131
+        0x33C6  => array(0x63, 0x2215, 0x6B, 0x67),
1132
+        0x33C7  => array(0x63, 0x6F, 0x2E),
1133
+        0x33C8  => array(0x64, 0x62),
1134
+        0x33C9  => array(0x67, 0x79),
1135
+        0x33CB  => array(0x68, 0x70),
1136
+        0x33CD  => array(0x6B, 0x6B),
1137
+        0x33CE  => array(0x6B, 0x6D),
1138
+        0x33D7  => array(0x70, 0x68),
1139
+        0x33D9  => array(0x70, 0x70, 0x6D),
1140
+        0x33DA  => array(0x70, 0x72),
1141
+        0x33DC  => array(0x73, 0x76),
1142
+        0x33DD  => array(0x77, 0x62),
1143
+        0xFB00  => array(0x66, 0x66),
1144
+        0xFB01  => array(0x66, 0x69),
1145
+        0xFB02  => array(0x66, 0x6C),
1146
+        0xFB03  => array(0x66, 0x66, 0x69),
1147
+        0xFB04  => array(0x66, 0x66, 0x6C),
1148
+        0xFB05  => array(0x73, 0x74),
1149
+        0xFB06  => array(0x73, 0x74),
1150
+        0xFB13  => array(0x574, 0x576),
1151
+        0xFB14  => array(0x574, 0x565),
1152
+        0xFB15  => array(0x574, 0x56B),
1153
+        0xFB16  => array(0x57E, 0x576),
1154
+        0xFB17  => array(0x574, 0x56D),
1155
+        0xFF21  => array(0xFF41),
1156
+        0xFF22  => array(0xFF42),
1157
+        0xFF23  => array(0xFF43),
1158
+        0xFF24  => array(0xFF44),
1159
+        0xFF25  => array(0xFF45),
1160
+        0xFF26  => array(0xFF46),
1161
+        0xFF27  => array(0xFF47),
1162
+        0xFF28  => array(0xFF48),
1163
+        0xFF29  => array(0xFF49),
1164
+        0xFF2A  => array(0xFF4A),
1165
+        0xFF2B  => array(0xFF4B),
1166
+        0xFF2C  => array(0xFF4C),
1167
+        0xFF2D  => array(0xFF4D),
1168
+        0xFF2E  => array(0xFF4E),
1169
+        0xFF2F  => array(0xFF4F),
1170
+        0xFF30  => array(0xFF50),
1171
+        0xFF31  => array(0xFF51),
1172
+        0xFF32  => array(0xFF52),
1173
+        0xFF33  => array(0xFF53),
1174
+        0xFF34  => array(0xFF54),
1175
+        0xFF35  => array(0xFF55),
1176
+        0xFF36  => array(0xFF56),
1177
+        0xFF37  => array(0xFF57),
1178
+        0xFF38  => array(0xFF58),
1179
+        0xFF39  => array(0xFF59),
1180
+        0xFF3A  => array(0xFF5A),
1181
+        0x10400 => array(0x10428),
1182
+        0x10401 => array(0x10429),
1183
+        0x10402 => array(0x1042A),
1184
+        0x10403 => array(0x1042B),
1185
+        0x10404 => array(0x1042C),
1186
+        0x10405 => array(0x1042D),
1187
+        0x10406 => array(0x1042E),
1188
+        0x10407 => array(0x1042F),
1189
+        0x10408 => array(0x10430),
1190
+        0x10409 => array(0x10431),
1191
+        0x1040A => array(0x10432),
1192
+        0x1040B => array(0x10433),
1193
+        0x1040C => array(0x10434),
1194
+        0x1040D => array(0x10435),
1195
+        0x1040E => array(0x10436),
1196
+        0x1040F => array(0x10437),
1197
+        0x10410 => array(0x10438),
1198
+        0x10411 => array(0x10439),
1199
+        0x10412 => array(0x1043A),
1200
+        0x10413 => array(0x1043B),
1201
+        0x10414 => array(0x1043C),
1202
+        0x10415 => array(0x1043D),
1203
+        0x10416 => array(0x1043E),
1204
+        0x10417 => array(0x1043F),
1205
+        0x10418 => array(0x10440),
1206
+        0x10419 => array(0x10441),
1207
+        0x1041A => array(0x10442),
1208
+        0x1041B => array(0x10443),
1209
+        0x1041C => array(0x10444),
1210
+        0x1041D => array(0x10445),
1211
+        0x1041E => array(0x10446),
1212
+        0x1041F => array(0x10447),
1213
+        0x10420 => array(0x10448),
1214
+        0x10421 => array(0x10449),
1215
+        0x10422 => array(0x1044A),
1216
+        0x10423 => array(0x1044B),
1217
+        0x10424 => array(0x1044C),
1218
+        0x10425 => array(0x1044D),
1219
+        0x1D400 => array(0x61),
1220
+        0x1D401 => array(0x62),
1221
+        0x1D402 => array(0x63),
1222
+        0x1D403 => array(0x64),
1223
+        0x1D404 => array(0x65),
1224
+        0x1D405 => array(0x66),
1225
+        0x1D406 => array(0x67),
1226
+        0x1D407 => array(0x68),
1227
+        0x1D408 => array(0x69),
1228
+        0x1D409 => array(0x6A),
1229
+        0x1D40A => array(0x6B),
1230
+        0x1D40B => array(0x6C),
1231
+        0x1D40C => array(0x6D),
1232
+        0x1D40D => array(0x6E),
1233
+        0x1D40E => array(0x6F),
1234
+        0x1D40F => array(0x70),
1235
+        0x1D410 => array(0x71),
1236
+        0x1D411 => array(0x72),
1237
+        0x1D412 => array(0x73),
1238
+        0x1D413 => array(0x74),
1239
+        0x1D414 => array(0x75),
1240
+        0x1D415 => array(0x76),
1241
+        0x1D416 => array(0x77),
1242
+        0x1D417 => array(0x78),
1243
+        0x1D418 => array(0x79),
1244
+        0x1D419 => array(0x7A),
1245
+        0x1D434 => array(0x61),
1246
+        0x1D435 => array(0x62),
1247
+        0x1D436 => array(0x63),
1248
+        0x1D437 => array(0x64),
1249
+        0x1D438 => array(0x65),
1250
+        0x1D439 => array(0x66),
1251
+        0x1D43A => array(0x67),
1252
+        0x1D43B => array(0x68),
1253
+        0x1D43C => array(0x69),
1254
+        0x1D43D => array(0x6A),
1255
+        0x1D43E => array(0x6B),
1256
+        0x1D43F => array(0x6C),
1257
+        0x1D440 => array(0x6D),
1258
+        0x1D441 => array(0x6E),
1259
+        0x1D442 => array(0x6F),
1260
+        0x1D443 => array(0x70),
1261
+        0x1D444 => array(0x71),
1262
+        0x1D445 => array(0x72),
1263
+        0x1D446 => array(0x73),
1264
+        0x1D447 => array(0x74),
1265
+        0x1D448 => array(0x75),
1266
+        0x1D449 => array(0x76),
1267
+        0x1D44A => array(0x77),
1268
+        0x1D44B => array(0x78),
1269
+        0x1D44C => array(0x79),
1270
+        0x1D44D => array(0x7A),
1271
+        0x1D468 => array(0x61),
1272
+        0x1D469 => array(0x62),
1273
+        0x1D46A => array(0x63),
1274
+        0x1D46B => array(0x64),
1275
+        0x1D46C => array(0x65),
1276
+        0x1D46D => array(0x66),
1277
+        0x1D46E => array(0x67),
1278
+        0x1D46F => array(0x68),
1279
+        0x1D470 => array(0x69),
1280
+        0x1D471 => array(0x6A),
1281
+        0x1D472 => array(0x6B),
1282
+        0x1D473 => array(0x6C),
1283
+        0x1D474 => array(0x6D),
1284
+        0x1D475 => array(0x6E),
1285
+        0x1D476 => array(0x6F),
1286
+        0x1D477 => array(0x70),
1287
+        0x1D478 => array(0x71),
1288
+        0x1D479 => array(0x72),
1289
+        0x1D47A => array(0x73),
1290
+        0x1D47B => array(0x74),
1291
+        0x1D47C => array(0x75),
1292
+        0x1D47D => array(0x76),
1293
+        0x1D47E => array(0x77),
1294
+        0x1D47F => array(0x78),
1295
+        0x1D480 => array(0x79),
1296
+        0x1D481 => array(0x7A),
1297
+        0x1D49C => array(0x61),
1298
+        0x1D49E => array(0x63),
1299
+        0x1D49F => array(0x64),
1300
+        0x1D4A2 => array(0x67),
1301
+        0x1D4A5 => array(0x6A),
1302
+        0x1D4A6 => array(0x6B),
1303
+        0x1D4A9 => array(0x6E),
1304
+        0x1D4AA => array(0x6F),
1305
+        0x1D4AB => array(0x70),
1306
+        0x1D4AC => array(0x71),
1307
+        0x1D4AE => array(0x73),
1308
+        0x1D4AF => array(0x74),
1309
+        0x1D4B0 => array(0x75),
1310
+        0x1D4B1 => array(0x76),
1311
+        0x1D4B2 => array(0x77),
1312
+        0x1D4B3 => array(0x78),
1313
+        0x1D4B4 => array(0x79),
1314
+        0x1D4B5 => array(0x7A),
1315
+        0x1D4D0 => array(0x61),
1316
+        0x1D4D1 => array(0x62),
1317
+        0x1D4D2 => array(0x63),
1318
+        0x1D4D3 => array(0x64),
1319
+        0x1D4D4 => array(0x65),
1320
+        0x1D4D5 => array(0x66),
1321
+        0x1D4D6 => array(0x67),
1322
+        0x1D4D7 => array(0x68),
1323
+        0x1D4D8 => array(0x69),
1324
+        0x1D4D9 => array(0x6A),
1325
+        0x1D4DA => array(0x6B),
1326
+        0x1D4DB => array(0x6C),
1327
+        0x1D4DC => array(0x6D),
1328
+        0x1D4DD => array(0x6E),
1329
+        0x1D4DE => array(0x6F),
1330
+        0x1D4DF => array(0x70),
1331
+        0x1D4E0 => array(0x71),
1332
+        0x1D4E1 => array(0x72),
1333
+        0x1D4E2 => array(0x73),
1334
+        0x1D4E3 => array(0x74),
1335
+        0x1D4E4 => array(0x75),
1336
+        0x1D4E5 => array(0x76),
1337
+        0x1D4E6 => array(0x77),
1338
+        0x1D4E7 => array(0x78),
1339
+        0x1D4E8 => array(0x79),
1340
+        0x1D4E9 => array(0x7A),
1341
+        0x1D504 => array(0x61),
1342
+        0x1D505 => array(0x62),
1343
+        0x1D507 => array(0x64),
1344
+        0x1D508 => array(0x65),
1345
+        0x1D509 => array(0x66),
1346
+        0x1D50A => array(0x67),
1347
+        0x1D50D => array(0x6A),
1348
+        0x1D50E => array(0x6B),
1349
+        0x1D50F => array(0x6C),
1350
+        0x1D510 => array(0x6D),
1351
+        0x1D511 => array(0x6E),
1352
+        0x1D512 => array(0x6F),
1353
+        0x1D513 => array(0x70),
1354
+        0x1D514 => array(0x71),
1355
+        0x1D516 => array(0x73),
1356
+        0x1D517 => array(0x74),
1357
+        0x1D518 => array(0x75),
1358
+        0x1D519 => array(0x76),
1359
+        0x1D51A => array(0x77),
1360
+        0x1D51B => array(0x78),
1361
+        0x1D51C => array(0x79),
1362
+        0x1D538 => array(0x61),
1363
+        0x1D539 => array(0x62),
1364
+        0x1D53B => array(0x64),
1365
+        0x1D53C => array(0x65),
1366
+        0x1D53D => array(0x66),
1367
+        0x1D53E => array(0x67),
1368
+        0x1D540 => array(0x69),
1369
+        0x1D541 => array(0x6A),
1370
+        0x1D542 => array(0x6B),
1371
+        0x1D543 => array(0x6C),
1372
+        0x1D544 => array(0x6D),
1373
+        0x1D546 => array(0x6F),
1374
+        0x1D54A => array(0x73),
1375
+        0x1D54B => array(0x74),
1376
+        0x1D54C => array(0x75),
1377
+        0x1D54D => array(0x76),
1378
+        0x1D54E => array(0x77),
1379
+        0x1D54F => array(0x78),
1380
+        0x1D550 => array(0x79),
1381
+        0x1D56C => array(0x61),
1382
+        0x1D56D => array(0x62),
1383
+        0x1D56E => array(0x63),
1384
+        0x1D56F => array(0x64),
1385
+        0x1D570 => array(0x65),
1386
+        0x1D571 => array(0x66),
1387
+        0x1D572 => array(0x67),
1388
+        0x1D573 => array(0x68),
1389
+        0x1D574 => array(0x69),
1390
+        0x1D575 => array(0x6A),
1391
+        0x1D576 => array(0x6B),
1392
+        0x1D577 => array(0x6C),
1393
+        0x1D578 => array(0x6D),
1394
+        0x1D579 => array(0x6E),
1395
+        0x1D57A => array(0x6F),
1396
+        0x1D57B => array(0x70),
1397
+        0x1D57C => array(0x71),
1398
+        0x1D57D => array(0x72),
1399
+        0x1D57E => array(0x73),
1400
+        0x1D57F => array(0x74),
1401
+        0x1D580 => array(0x75),
1402
+        0x1D581 => array(0x76),
1403
+        0x1D582 => array(0x77),
1404
+        0x1D583 => array(0x78),
1405
+        0x1D584 => array(0x79),
1406
+        0x1D585 => array(0x7A),
1407
+        0x1D5A0 => array(0x61),
1408
+        0x1D5A1 => array(0x62),
1409
+        0x1D5A2 => array(0x63),
1410
+        0x1D5A3 => array(0x64),
1411
+        0x1D5A4 => array(0x65),
1412
+        0x1D5A5 => array(0x66),
1413
+        0x1D5A6 => array(0x67),
1414
+        0x1D5A7 => array(0x68),
1415
+        0x1D5A8 => array(0x69),
1416
+        0x1D5A9 => array(0x6A),
1417
+        0x1D5AA => array(0x6B),
1418
+        0x1D5AB => array(0x6C),
1419
+        0x1D5AC => array(0x6D),
1420
+        0x1D5AD => array(0x6E),
1421
+        0x1D5AE => array(0x6F),
1422
+        0x1D5AF => array(0x70),
1423
+        0x1D5B0 => array(0x71),
1424
+        0x1D5B1 => array(0x72),
1425
+        0x1D5B2 => array(0x73),
1426
+        0x1D5B3 => array(0x74),
1427
+        0x1D5B4 => array(0x75),
1428
+        0x1D5B5 => array(0x76),
1429
+        0x1D5B6 => array(0x77),
1430
+        0x1D5B7 => array(0x78),
1431
+        0x1D5B8 => array(0x79),
1432
+        0x1D5B9 => array(0x7A),
1433
+        0x1D5D4 => array(0x61),
1434
+        0x1D5D5 => array(0x62),
1435
+        0x1D5D6 => array(0x63),
1436
+        0x1D5D7 => array(0x64),
1437
+        0x1D5D8 => array(0x65),
1438
+        0x1D5D9 => array(0x66),
1439
+        0x1D5DA => array(0x67),
1440
+        0x1D5DB => array(0x68),
1441
+        0x1D5DC => array(0x69),
1442
+        0x1D5DD => array(0x6A),
1443
+        0x1D5DE => array(0x6B),
1444
+        0x1D5DF => array(0x6C),
1445
+        0x1D5E0 => array(0x6D),
1446
+        0x1D5E1 => array(0x6E),
1447
+        0x1D5E2 => array(0x6F),
1448
+        0x1D5E3 => array(0x70),
1449
+        0x1D5E4 => array(0x71),
1450
+        0x1D5E5 => array(0x72),
1451
+        0x1D5E6 => array(0x73),
1452
+        0x1D5E7 => array(0x74),
1453
+        0x1D5E8 => array(0x75),
1454
+        0x1D5E9 => array(0x76),
1455
+        0x1D5EA => array(0x77),
1456
+        0x1D5EB => array(0x78),
1457
+        0x1D5EC => array(0x79),
1458
+        0x1D5ED => array(0x7A),
1459
+        0x1D608 => array(0x61),
1460
+        0x1D609 => array(0x62),
1461
+        0x1D60A => array(0x63),
1462
+        0x1D60B => array(0x64),
1463
+        0x1D60C => array(0x65),
1464
+        0x1D60D => array(0x66),
1465
+        0x1D60E => array(0x67),
1466
+        0x1D60F => array(0x68),
1467
+        0x1D610 => array(0x69),
1468
+        0x1D611 => array(0x6A),
1469
+        0x1D612 => array(0x6B),
1470
+        0x1D613 => array(0x6C),
1471
+        0x1D614 => array(0x6D),
1472
+        0x1D615 => array(0x6E),
1473
+        0x1D616 => array(0x6F),
1474
+        0x1D617 => array(0x70),
1475
+        0x1D618 => array(0x71),
1476
+        0x1D619 => array(0x72),
1477
+        0x1D61A => array(0x73),
1478
+        0x1D61B => array(0x74),
1479
+        0x1D61C => array(0x75),
1480
+        0x1D61D => array(0x76),
1481
+        0x1D61E => array(0x77),
1482
+        0x1D61F => array(0x78),
1483
+        0x1D620 => array(0x79),
1484
+        0x1D621 => array(0x7A),
1485
+        0x1D63C => array(0x61),
1486
+        0x1D63D => array(0x62),
1487
+        0x1D63E => array(0x63),
1488
+        0x1D63F => array(0x64),
1489
+        0x1D640 => array(0x65),
1490
+        0x1D641 => array(0x66),
1491
+        0x1D642 => array(0x67),
1492
+        0x1D643 => array(0x68),
1493
+        0x1D644 => array(0x69),
1494
+        0x1D645 => array(0x6A),
1495
+        0x1D646 => array(0x6B),
1496
+        0x1D647 => array(0x6C),
1497
+        0x1D648 => array(0x6D),
1498
+        0x1D649 => array(0x6E),
1499
+        0x1D64A => array(0x6F),
1500
+        0x1D64B => array(0x70),
1501
+        0x1D64C => array(0x71),
1502
+        0x1D64D => array(0x72),
1503
+        0x1D64E => array(0x73),
1504
+        0x1D64F => array(0x74),
1505
+        0x1D650 => array(0x75),
1506
+        0x1D651 => array(0x76),
1507
+        0x1D652 => array(0x77),
1508
+        0x1D653 => array(0x78),
1509
+        0x1D654 => array(0x79),
1510
+        0x1D655 => array(0x7A),
1511
+        0x1D670 => array(0x61),
1512
+        0x1D671 => array(0x62),
1513
+        0x1D672 => array(0x63),
1514
+        0x1D673 => array(0x64),
1515
+        0x1D674 => array(0x65),
1516
+        0x1D675 => array(0x66),
1517
+        0x1D676 => array(0x67),
1518
+        0x1D677 => array(0x68),
1519
+        0x1D678 => array(0x69),
1520
+        0x1D679 => array(0x6A),
1521
+        0x1D67A => array(0x6B),
1522
+        0x1D67B => array(0x6C),
1523
+        0x1D67C => array(0x6D),
1524
+        0x1D67D => array(0x6E),
1525
+        0x1D67E => array(0x6F),
1526
+        0x1D67F => array(0x70),
1527
+        0x1D680 => array(0x71),
1528
+        0x1D681 => array(0x72),
1529
+        0x1D682 => array(0x73),
1530
+        0x1D683 => array(0x74),
1531
+        0x1D684 => array(0x75),
1532
+        0x1D685 => array(0x76),
1533
+        0x1D686 => array(0x77),
1534
+        0x1D687 => array(0x78),
1535
+        0x1D688 => array(0x79),
1536
+        0x1D689 => array(0x7A),
1537
+        0x1D6A8 => array(0x3B1),
1538
+        0x1D6A9 => array(0x3B2),
1539
+        0x1D6AA => array(0x3B3),
1540
+        0x1D6AB => array(0x3B4),
1541
+        0x1D6AC => array(0x3B5),
1542
+        0x1D6AD => array(0x3B6),
1543
+        0x1D6AE => array(0x3B7),
1544
+        0x1D6AF => array(0x3B8),
1545
+        0x1D6B0 => array(0x3B9),
1546
+        0x1D6B1 => array(0x3BA),
1547
+        0x1D6B2 => array(0x3BB),
1548
+        0x1D6B3 => array(0x3BC),
1549
+        0x1D6B4 => array(0x3BD),
1550
+        0x1D6B5 => array(0x3BE),
1551
+        0x1D6B6 => array(0x3BF),
1552
+        0x1D6B7 => array(0x3C0),
1553
+        0x1D6B8 => array(0x3C1),
1554
+        0x1D6B9 => array(0x3B8),
1555
+        0x1D6BA => array(0x3C3),
1556
+        0x1D6BB => array(0x3C4),
1557
+        0x1D6BC => array(0x3C5),
1558
+        0x1D6BD => array(0x3C6),
1559
+        0x1D6BE => array(0x3C7),
1560
+        0x1D6BF => array(0x3C8),
1561
+        0x1D6C0 => array(0x3C9),
1562
+        0x1D6D3 => array(0x3C3),
1563
+        0x1D6E2 => array(0x3B1),
1564
+        0x1D6E3 => array(0x3B2),
1565
+        0x1D6E4 => array(0x3B3),
1566
+        0x1D6E5 => array(0x3B4),
1567
+        0x1D6E6 => array(0x3B5),
1568
+        0x1D6E7 => array(0x3B6),
1569
+        0x1D6E8 => array(0x3B7),
1570
+        0x1D6E9 => array(0x3B8),
1571
+        0x1D6EA => array(0x3B9),
1572
+        0x1D6EB => array(0x3BA),
1573
+        0x1D6EC => array(0x3BB),
1574
+        0x1D6ED => array(0x3BC),
1575
+        0x1D6EE => array(0x3BD),
1576
+        0x1D6EF => array(0x3BE),
1577
+        0x1D6F0 => array(0x3BF),
1578
+        0x1D6F1 => array(0x3C0),
1579
+        0x1D6F2 => array(0x3C1),
1580
+        0x1D6F3 => array(0x3B8),
1581
+        0x1D6F4 => array(0x3C3),
1582
+        0x1D6F5 => array(0x3C4),
1583
+        0x1D6F6 => array(0x3C5),
1584
+        0x1D6F7 => array(0x3C6),
1585
+        0x1D6F8 => array(0x3C7),
1586
+        0x1D6F9 => array(0x3C8),
1587
+        0x1D6FA => array(0x3C9),
1588
+        0x1D70D => array(0x3C3),
1589
+        0x1D71C => array(0x3B1),
1590
+        0x1D71D => array(0x3B2),
1591
+        0x1D71E => array(0x3B3),
1592
+        0x1D71F => array(0x3B4),
1593
+        0x1D720 => array(0x3B5),
1594
+        0x1D721 => array(0x3B6),
1595
+        0x1D722 => array(0x3B7),
1596
+        0x1D723 => array(0x3B8),
1597
+        0x1D724 => array(0x3B9),
1598
+        0x1D725 => array(0x3BA),
1599
+        0x1D726 => array(0x3BB),
1600
+        0x1D727 => array(0x3BC),
1601
+        0x1D728 => array(0x3BD),
1602
+        0x1D729 => array(0x3BE),
1603
+        0x1D72A => array(0x3BF),
1604
+        0x1D72B => array(0x3C0),
1605
+        0x1D72C => array(0x3C1),
1606
+        0x1D72D => array(0x3B8),
1607
+        0x1D72E => array(0x3C3),
1608
+        0x1D72F => array(0x3C4),
1609
+        0x1D730 => array(0x3C5),
1610
+        0x1D731 => array(0x3C6),
1611
+        0x1D732 => array(0x3C7),
1612
+        0x1D733 => array(0x3C8),
1613
+        0x1D734 => array(0x3C9),
1614
+        0x1D747 => array(0x3C3),
1615
+        0x1D756 => array(0x3B1),
1616
+        0x1D757 => array(0x3B2),
1617
+        0x1D758 => array(0x3B3),
1618
+        0x1D759 => array(0x3B4),
1619
+        0x1D75A => array(0x3B5),
1620
+        0x1D75B => array(0x3B6),
1621
+        0x1D75C => array(0x3B7),
1622
+        0x1D75D => array(0x3B8),
1623
+        0x1D75E => array(0x3B9),
1624
+        0x1D75F => array(0x3BA),
1625
+        0x1D760 => array(0x3BB),
1626
+        0x1D761 => array(0x3BC),
1627
+        0x1D762 => array(0x3BD),
1628
+        0x1D763 => array(0x3BE),
1629
+        0x1D764 => array(0x3BF),
1630
+        0x1D765 => array(0x3C0),
1631
+        0x1D766 => array(0x3C1),
1632
+        0x1D767 => array(0x3B8),
1633
+        0x1D768 => array(0x3C3),
1634
+        0x1D769 => array(0x3C4),
1635
+        0x1D76A => array(0x3C5),
1636
+        0x1D76B => array(0x3C6),
1637
+        0x1D76C => array(0x3C7),
1638
+        0x1D76D => array(0x3C8),
1639
+        0x1D76E => array(0x3C9),
1640
+        0x1D781 => array(0x3C3),
1641
+        0x1D790 => array(0x3B1),
1642
+        0x1D791 => array(0x3B2),
1643
+        0x1D792 => array(0x3B3),
1644
+        0x1D793 => array(0x3B4),
1645
+        0x1D794 => array(0x3B5),
1646
+        0x1D795 => array(0x3B6),
1647
+        0x1D796 => array(0x3B7),
1648
+        0x1D797 => array(0x3B8),
1649
+        0x1D798 => array(0x3B9),
1650
+        0x1D799 => array(0x3BA),
1651
+        0x1D79A => array(0x3BB),
1652
+        0x1D79B => array(0x3BC),
1653
+        0x1D79C => array(0x3BD),
1654
+        0x1D79D => array(0x3BE),
1655
+        0x1D79E => array(0x3BF),
1656
+        0x1D79F => array(0x3C0),
1657
+        0x1D7A0 => array(0x3C1),
1658
+        0x1D7A1 => array(0x3B8),
1659
+        0x1D7A2 => array(0x3C3),
1660
+        0x1D7A3 => array(0x3C4),
1661
+        0x1D7A4 => array(0x3C5),
1662
+        0x1D7A5 => array(0x3C6),
1663
+        0x1D7A6 => array(0x3C7),
1664
+        0x1D7A7 => array(0x3C8),
1665
+        0x1D7A8 => array(0x3C9),
1666
+        0x1D7BB => array(0x3C3),
1667
+        0x3F9   => array(0x3C3),
1668
+        0x1D2C  => array(0x61),
1669
+        0x1D2D  => array(0xE6),
1670
+        0x1D2E  => array(0x62),
1671
+        0x1D30  => array(0x64),
1672
+        0x1D31  => array(0x65),
1673
+        0x1D32  => array(0x1DD),
1674
+        0x1D33  => array(0x67),
1675
+        0x1D34  => array(0x68),
1676
+        0x1D35  => array(0x69),
1677
+        0x1D36  => array(0x6A),
1678
+        0x1D37  => array(0x6B),
1679
+        0x1D38  => array(0x6C),
1680
+        0x1D39  => array(0x6D),
1681
+        0x1D3A  => array(0x6E),
1682
+        0x1D3C  => array(0x6F),
1683
+        0x1D3D  => array(0x223),
1684
+        0x1D3E  => array(0x70),
1685
+        0x1D3F  => array(0x72),
1686
+        0x1D40  => array(0x74),
1687
+        0x1D41  => array(0x75),
1688
+        0x1D42  => array(0x77),
1689
+        0x213B  => array(0x66, 0x61, 0x78),
1690
+        0x3250  => array(0x70, 0x74, 0x65),
1691
+        0x32CC  => array(0x68, 0x67),
1692
+        0x32CE  => array(0x65, 0x76),
1693
+        0x32CF  => array(0x6C, 0x74, 0x64),
1694
+        0x337A  => array(0x69, 0x75),
1695
+        0x33DE  => array(0x76, 0x2215, 0x6D),
1696
+        0x33DF  => array(0x61, 0x2215, 0x6D)
1697
+    );
1698
+
1699
+    /**
1700
+     * Normalization Combining Classes; Code Points not listed
1701
+     * got Combining Class 0.
1702
+     *
1703
+     * @static
1704
+     * @var array
1705
+     * @access private
1706
+     */
1707
+    private static $_np_norm_combcls = array(
1708
+        0x334   => 1,
1709
+        0x335   => 1,
1710
+        0x336   => 1,
1711
+        0x337   => 1,
1712
+        0x338   => 1,
1713
+        0x93C   => 7,
1714
+        0x9BC   => 7,
1715
+        0xA3C   => 7,
1716
+        0xABC   => 7,
1717
+        0xB3C   => 7,
1718
+        0xCBC   => 7,
1719
+        0x1037  => 7,
1720
+        0x3099  => 8,
1721
+        0x309A  => 8,
1722
+        0x94D   => 9,
1723
+        0x9CD   => 9,
1724
+        0xA4D   => 9,
1725
+        0xACD   => 9,
1726
+        0xB4D   => 9,
1727
+        0xBCD   => 9,
1728
+        0xC4D   => 9,
1729
+        0xCCD   => 9,
1730
+        0xD4D   => 9,
1731
+        0xDCA   => 9,
1732
+        0xE3A   => 9,
1733
+        0xF84   => 9,
1734
+        0x1039  => 9,
1735
+        0x1714  => 9,
1736
+        0x1734  => 9,
1737
+        0x17D2  => 9,
1738
+        0x5B0   => 10,
1739
+        0x5B1   => 11,
1740
+        0x5B2   => 12,
1741
+        0x5B3   => 13,
1742
+        0x5B4   => 14,
1743
+        0x5B5   => 15,
1744
+        0x5B6   => 16,
1745
+        0x5B7   => 17,
1746
+        0x5B8   => 18,
1747
+        0x5B9   => 19,
1748
+        0x5BB   => 20,
1749
+        0x5Bc   => 21,
1750
+        0x5BD   => 22,
1751
+        0x5BF   => 23,
1752
+        0x5C1   => 24,
1753
+        0x5C2   => 25,
1754
+        0xFB1E  => 26,
1755
+        0x64B   => 27,
1756
+        0x64C   => 28,
1757
+        0x64D   => 29,
1758
+        0x64E   => 30,
1759
+        0x64F   => 31,
1760
+        0x650   => 32,
1761
+        0x651   => 33,
1762
+        0x652   => 34,
1763
+        0x670   => 35,
1764
+        0x711   => 36,
1765
+        0xC55   => 84,
1766
+        0xC56   => 91,
1767
+        0xE38   => 103,
1768
+        0xE39   => 103,
1769
+        0xE48   => 107,
1770
+        0xE49   => 107,
1771
+        0xE4A   => 107,
1772
+        0xE4B   => 107,
1773
+        0xEB8   => 118,
1774
+        0xEB9   => 118,
1775
+        0xEC8   => 122,
1776
+        0xEC9   => 122,
1777
+        0xECA   => 122,
1778
+        0xECB   => 122,
1779
+        0xF71   => 129,
1780
+        0xF72   => 130,
1781
+        0xF7A   => 130,
1782
+        0xF7B   => 130,
1783
+        0xF7C   => 130,
1784
+        0xF7D   => 130,
1785
+        0xF80   => 130,
1786
+        0xF74   => 132,
1787
+        0x321   => 202,
1788
+        0x322   => 202,
1789
+        0x327   => 202,
1790
+        0x328   => 202,
1791
+        0x31B   => 216,
1792
+        0xF39   => 216,
1793
+        0x1D165 => 216,
1794
+        0x1D166 => 216,
1795
+        0x1D16E => 216,
1796
+        0x1D16F => 216,
1797
+        0x1D170 => 216,
1798
+        0x1D171 => 216,
1799
+        0x1D172 => 216,
1800
+        0x302A  => 218,
1801
+        0x316   => 220,
1802
+        0x317   => 220,
1803
+        0x318   => 220,
1804
+        0x319   => 220,
1805
+        0x31C   => 220,
1806
+        0x31D   => 220,
1807
+        0x31E   => 220,
1808
+        0x31F   => 220,
1809
+        0x320   => 220,
1810
+        0x323   => 220,
1811
+        0x324   => 220,
1812
+        0x325   => 220,
1813
+        0x326   => 220,
1814
+        0x329   => 220,
1815
+        0x32A   => 220,
1816
+        0x32B   => 220,
1817
+        0x32C   => 220,
1818
+        0x32D   => 220,
1819
+        0x32E   => 220,
1820
+        0x32F   => 220,
1821
+        0x330   => 220,
1822
+        0x331   => 220,
1823
+        0x332   => 220,
1824
+        0x333   => 220,
1825
+        0x339   => 220,
1826
+        0x33A   => 220,
1827
+        0x33B   => 220,
1828
+        0x33C   => 220,
1829
+        0x347   => 220,
1830
+        0x348   => 220,
1831
+        0x349   => 220,
1832
+        0x34D   => 220,
1833
+        0x34E   => 220,
1834
+        0x353   => 220,
1835
+        0x354   => 220,
1836
+        0x355   => 220,
1837
+        0x356   => 220,
1838
+        0x591   => 220,
1839
+        0x596   => 220,
1840
+        0x59B   => 220,
1841
+        0x5A3   => 220,
1842
+        0x5A4   => 220,
1843
+        0x5A5   => 220,
1844
+        0x5A6   => 220,
1845
+        0x5A7   => 220,
1846
+        0x5AA   => 220,
1847
+        0x655   => 220,
1848
+        0x656   => 220,
1849
+        0x6E3   => 220,
1850
+        0x6EA   => 220,
1851
+        0x6ED   => 220,
1852
+        0x731   => 220,
1853
+        0x734   => 220,
1854
+        0x737   => 220,
1855
+        0x738   => 220,
1856
+        0x739   => 220,
1857
+        0x73B   => 220,
1858
+        0x73C   => 220,
1859
+        0x73E   => 220,
1860
+        0x742   => 220,
1861
+        0x744   => 220,
1862
+        0x746   => 220,
1863
+        0x748   => 220,
1864
+        0x952   => 220,
1865
+        0xF18   => 220,
1866
+        0xF19   => 220,
1867
+        0xF35   => 220,
1868
+        0xF37   => 220,
1869
+        0xFC6   => 220,
1870
+        0x193B  => 220,
1871
+        0x20E8  => 220,
1872
+        0x1D17B => 220,
1873
+        0x1D17C => 220,
1874
+        0x1D17D => 220,
1875
+        0x1D17E => 220,
1876
+        0x1D17F => 220,
1877
+        0x1D180 => 220,
1878
+        0x1D181 => 220,
1879
+        0x1D182 => 220,
1880
+        0x1D18A => 220,
1881
+        0x1D18B => 220,
1882
+        0x59A   => 222,
1883
+        0x5AD   => 222,
1884
+        0x1929  => 222,
1885
+        0x302D  => 222,
1886
+        0x302E  => 224,
1887
+        0x302F  => 224,
1888
+        0x1D16D => 226,
1889
+        0x5AE   => 228,
1890
+        0x18A9  => 228,
1891
+        0x302B  => 228,
1892
+        0x300   => 230,
1893
+        0x301   => 230,
1894
+        0x302   => 230,
1895
+        0x303   => 230,
1896
+        0x304   => 230,
1897
+        0x305   => 230,
1898
+        0x306   => 230,
1899
+        0x307   => 230,
1900
+        0x308   => 230,
1901
+        0x309   => 230,
1902
+        0x30A   => 230,
1903
+        0x30B   => 230,
1904
+        0x30C   => 230,
1905
+        0x30D   => 230,
1906
+        0x30E   => 230,
1907
+        0x30F   => 230,
1908
+        0x310   => 230,
1909
+        0x311   => 230,
1910
+        0x312   => 230,
1911
+        0x313   => 230,
1912
+        0x314   => 230,
1913
+        0x33D   => 230,
1914
+        0x33E   => 230,
1915
+        0x33F   => 230,
1916
+        0x340   => 230,
1917
+        0x341   => 230,
1918
+        0x342   => 230,
1919
+        0x343   => 230,
1920
+        0x344   => 230,
1921
+        0x346   => 230,
1922
+        0x34A   => 230,
1923
+        0x34B   => 230,
1924
+        0x34C   => 230,
1925
+        0x350   => 230,
1926
+        0x351   => 230,
1927
+        0x352   => 230,
1928
+        0x357   => 230,
1929
+        0x363   => 230,
1930
+        0x364   => 230,
1931
+        0x365   => 230,
1932
+        0x366   => 230,
1933
+        0x367   => 230,
1934
+        0x368   => 230,
1935
+        0x369   => 230,
1936
+        0x36A   => 230,
1937
+        0x36B   => 230,
1938
+        0x36C   => 230,
1939
+        0x36D   => 230,
1940
+        0x36E   => 230,
1941
+        0x36F   => 230,
1942
+        0x483   => 230,
1943
+        0x484   => 230,
1944
+        0x485   => 230,
1945
+        0x486   => 230,
1946
+        0x592   => 230,
1947
+        0x593   => 230,
1948
+        0x594   => 230,
1949
+        0x595   => 230,
1950
+        0x597   => 230,
1951
+        0x598   => 230,
1952
+        0x599   => 230,
1953
+        0x59C   => 230,
1954
+        0x59D   => 230,
1955
+        0x59E   => 230,
1956
+        0x59F   => 230,
1957
+        0x5A0   => 230,
1958
+        0x5A1   => 230,
1959
+        0x5A8   => 230,
1960
+        0x5A9   => 230,
1961
+        0x5AB   => 230,
1962
+        0x5AC   => 230,
1963
+        0x5AF   => 230,
1964
+        0x5C4   => 230,
1965
+        0x610   => 230,
1966
+        0x611   => 230,
1967
+        0x612   => 230,
1968
+        0x613   => 230,
1969
+        0x614   => 230,
1970
+        0x615   => 230,
1971
+        0x653   => 230,
1972
+        0x654   => 230,
1973
+        0x657   => 230,
1974
+        0x658   => 230,
1975
+        0x6D6   => 230,
1976
+        0x6D7   => 230,
1977
+        0x6D8   => 230,
1978
+        0x6D9   => 230,
1979
+        0x6DA   => 230,
1980
+        0x6DB   => 230,
1981
+        0x6DC   => 230,
1982
+        0x6DF   => 230,
1983
+        0x6E0   => 230,
1984
+        0x6E1   => 230,
1985
+        0x6E2   => 230,
1986
+        0x6E4   => 230,
1987
+        0x6E7   => 230,
1988
+        0x6E8   => 230,
1989
+        0x6EB   => 230,
1990
+        0x6EC   => 230,
1991
+        0x730   => 230,
1992
+        0x732   => 230,
1993
+        0x733   => 230,
1994
+        0x735   => 230,
1995
+        0x736   => 230,
1996
+        0x73A   => 230,
1997
+        0x73D   => 230,
1998
+        0x73F   => 230,
1999
+        0x740   => 230,
2000
+        0x741   => 230,
2001
+        0x743   => 230,
2002
+        0x745   => 230,
2003
+        0x747   => 230,
2004
+        0x749   => 230,
2005
+        0x74A   => 230,
2006
+        0x951   => 230,
2007
+        0x953   => 230,
2008
+        0x954   => 230,
2009
+        0xF82   => 230,
2010
+        0xF83   => 230,
2011
+        0xF86   => 230,
2012
+        0xF87   => 230,
2013
+        0x170D  => 230,
2014
+        0x193A  => 230,
2015
+        0x20D0  => 230,
2016
+        0x20D1  => 230,
2017
+        0x20D4  => 230,
2018
+        0x20D5  => 230,
2019
+        0x20D6  => 230,
2020
+        0x20D7  => 230,
2021
+        0x20DB  => 230,
2022
+        0x20DC  => 230,
2023
+        0x20E1  => 230,
2024
+        0x20E7  => 230,
2025
+        0x20E9  => 230,
2026
+        0xFE20  => 230,
2027
+        0xFE21  => 230,
2028
+        0xFE22  => 230,
2029
+        0xFE23  => 230,
2030
+        0x1D185 => 230,
2031
+        0x1D186 => 230,
2032
+        0x1D187 => 230,
2033
+        0x1D189 => 230,
2034
+        0x1D188 => 230,
2035
+        0x1D1AA => 230,
2036
+        0x1D1AB => 230,
2037
+        0x1D1AC => 230,
2038
+        0x1D1AD => 230,
2039
+        0x315   => 232,
2040
+        0x31A   => 232,
2041
+        0x302C  => 232,
2042
+        0x35F   => 233,
2043
+        0x362   => 233,
2044
+        0x35D   => 234,
2045
+        0x35E   => 234,
2046
+        0x360   => 234,
2047
+        0x361   => 234,
2048
+        0x345   => 240
2049
+    );
2050
+    // }}}
2051
+
2052
+    // {{{ properties
2053
+    /**
2054
+     * @var string
2055
+     * @access private
2056
+     */
2057
+    private $_punycode_prefix = 'xn--';
2058
+
2059
+    /**
2060
+     * @access private
2061
+     */
2062
+    private $_invalid_ucs = 0x80000000;
2063
+
2064
+    /**
2065
+     * @access private
2066
+     */
2067
+    private $_max_ucs = 0x10FFFF;
2068
+
2069
+    /**
2070
+     * @var int
2071
+     * @access private
2072
+     */
2073
+    private $_base = 36;
2074
+
2075
+    /**
2076
+     * @var int
2077
+     * @access private
2078
+     */
2079
+    private $_tmin = 1;
2080
+
2081
+    /**
2082
+     * @var int
2083
+     * @access private
2084
+     */
2085
+    private $_tmax = 26;
2086
+
2087
+    /**
2088
+     * @var int
2089
+     * @access private
2090
+     */
2091
+    private $_skew = 38;
2092
+
2093
+    /**
2094
+     * @var int
2095
+     * @access private
2096
+     */
2097
+    private $_damp = 700;
2098
+
2099
+    /**
2100
+     * @var int
2101
+     * @access private
2102
+     */
2103
+    private $_initial_bias = 72;
2104
+
2105
+    /**
2106
+     * @var int
2107
+     * @access private
2108
+     */
2109
+    private $_initial_n = 0x80;
2110
+
2111
+    /**
2112
+     * @var int
2113
+     * @access private
2114
+     */
2115
+    private $_slast;
2116
+
2117
+    /**
2118
+     * @access private
2119
+     */
2120
+    private $_sbase = 0xAC00;
2121
+
2122
+    /**
2123
+     * @access private
2124
+     */
2125
+    private $_lbase = 0x1100;
2126
+
2127
+    /**
2128
+     * @access private
2129
+     */
2130
+    private $_vbase = 0x1161;
2131
+
2132
+    /**
2133
+     * @access private
2134
+     */
2135
+    private $_tbase = 0x11a7;
2136
+
2137
+    /**
2138
+     * @var int
2139
+     * @access private
2140
+     */
2141
+    private $_lcount = 19;
2142
+
2143
+    /**
2144
+     * @var int
2145
+     * @access private
2146
+     */
2147
+    private $_vcount = 21;
2148
+
2149
+    /**
2150
+     * @var int
2151
+     * @access private
2152
+     */
2153
+    private $_tcount = 28;
2154
+
2155
+    /**
2156
+     * vcount * tcount
2157
+     *
2158
+     * @var int
2159
+     * @access private
2160
+     */
2161
+    private $_ncount = 588;
2162
+
2163
+    /**
2164
+     * lcount * tcount * vcount
2165
+     *
2166
+     * @var int
2167
+     * @access private
2168
+     */
2169
+    private $_scount = 11172;
2170
+
2171
+    /**
2172
+     * Default encoding for encode()'s input and decode()'s output is UTF-8;
2173
+     * Other possible encodings are ucs4_string and ucs4_array
2174
+     * See {@link setParams()} for how to select these
2175
+     *
2176
+     * @var bool
2177
+     * @access private
2178
+     */
2179
+    private $_api_encoding = 'utf8';
2180
+
2181
+    /**
2182
+     * Overlong UTF-8 encodings are forbidden
2183
+     *
2184
+     * @var bool
2185
+     * @access private
2186
+     */
2187
+    private $_allow_overlong = false;
2188
+
2189
+    /**
2190
+     * Behave strict or not
2191
+     *
2192
+     * @var bool
2193
+     * @access private
2194
+     */
2195
+    private $_strict_mode = false;
2196
+
2197
+    /**
2198
+     * IDNA-version to use
2199
+     *
2200
+     * Values are "2003" and "2008".
2201
+     * Defaults to "2003", since that was the original version and for
2202
+     * compatibility with previous versions of this library.
2203
+     * If you need to encode "new" characters like the German "Eszett",
2204
+     * please switch to 2008 first before encoding.
2205
+     *
2206
+     * @var bool
2207
+     * @access private
2208
+     */
2209
+    private $_version = '2003';
2210
+
2211
+    /**
2212
+     * Cached value indicating whether or not mbstring function overloading is
2213
+     * on for strlen
2214
+     *
2215
+     * This is cached for optimal performance.
2216
+     *
2217
+     * @var boolean
2218
+     * @see Net_IDNA2::_byteLength()
2219
+     */
2220
+    private static $_mb_string_overload = null;
2221
+    // }}}
2222
+
2223
+
2224
+    // {{{ constructor
2225
+    /**
2226
+     * Constructor
2227
+     *
2228
+     * @param array $options Options to initialise the object with
2229
+     *
2230
+     * @access public
2231
+     * @see    setParams()
2232
+     */
2233
+    public function __construct($options = null)
2234
+    {
2235
+        $this->_slast = $this->_sbase + $this->_lcount * $this->_vcount * $this->_tcount;
2236
+
2237
+        if (is_array($options)) {
2238
+            $this->setParams($options);
2239
+        }
2240
+
2241
+        // populate mbstring overloading cache if not set
2242
+        if (self::$_mb_string_overload === null) {
2243
+            self::$_mb_string_overload = (extension_loaded('mbstring')
2244
+                && (ini_get('mbstring.func_overload') & 0x02) === 0x02);
2245
+        }
2246
+    }
2247
+    // }}}
2248
+
2249
+
2250
+    /**
2251
+     * Sets a new option value. Available options and values:
2252
+     *
2253
+     * [utf8 -     Use either UTF-8 or ISO-8859-1 as input (true for UTF-8, false
2254
+     *             otherwise); The output is always UTF-8]
2255
+     * [overlong - Unicode does not allow unnecessarily long encodings of chars,
2256
+     *             to allow this, set this parameter to true, else to false;
2257
+     *             default is false.]
2258
+     * [strict -   true: strict mode, good for registration purposes - Causes errors
2259
+     *             on failures; false: loose mode, ideal for "wildlife" applications
2260
+     *             by silently ignoring errors and returning the original input instead]
2261
+     *
2262
+     * @param mixed  $option Parameter to set (string: single parameter; array of Parameter => Value pairs)
2263
+     * @param string $value  Value to use (if parameter 1 is a string)
2264
+     *
2265
+     * @return boolean       true on success, false otherwise
2266
+     * @access public
2267
+     */
2268
+    public function setParams($option, $value = false)
2269
+    {
2270
+        if (!is_array($option)) {
2271
+            $option = array($option => $value);
2272
+        }
2273
+
2274
+        foreach ($option as $k => $v) {
2275
+            switch ($k) {
2276
+            case 'encoding':
2277
+                switch ($v) {
2278
+                case 'utf8':
2279
+                case 'ucs4_string':
2280
+                case 'ucs4_array':
2281
+                    $this->_api_encoding = $v;
2282
+                    break;
2283
+
2284
+                default:
2285
+                    throw new InvalidArgumentException('Set Parameter: Unknown parameter '.$v.' for option '.$k);
2286
+                }
2287
+
2288
+                break;
2289
+
2290
+            case 'overlong':
2291
+                $this->_allow_overlong = ($v) ? true : false;
2292
+                break;
2293
+
2294
+            case 'strict':
2295
+                $this->_strict_mode = ($v) ? true : false;
2296
+                break;
2297
+
2298
+            case 'version':
2299
+                if (in_array($v, array('2003', '2008'))) {
2300
+                    $this->_version = $v;
2301
+                } else {
2302
+                    throw new InvalidArgumentException('Set Parameter: Invalid parameter '.$v.' for option '.$k);
2303
+                }
2304
+                break;
2305
+
2306
+            default:
2307
+                return false;
2308
+            }
2309
+        }
2310
+
2311
+        return true;
2312
+    }
2313
+
2314
+    /**
2315
+     * Encode a given UTF-8 domain name.
2316
+     *
2317
+     * @param string $decoded           Domain name (UTF-8 or UCS-4)
2318
+     * @param string $one_time_encoding Desired input encoding, see {@link set_parameter}
2319
+     *                                  If not given will use default-encoding
2320
+     *
2321
+     * @return string Encoded Domain name (ACE string)
2322
+     * @return mixed  processed string
2323
+     * @throws Exception
2324
+     * @access public
2325
+     */
2326
+    public function encode($decoded, $one_time_encoding = false)
2327
+    {
2328
+        // Forcing conversion of input to UCS4 array
2329
+        // If one time encoding is given, use this, else the objects property
2330
+        switch (($one_time_encoding) ? $one_time_encoding : $this->_api_encoding) {
2331
+        case 'utf8':
2332
+            $decoded = $this->_utf8_to_ucs4($decoded);
2333
+            break;
2334
+        case 'ucs4_string':
2335
+            $decoded = $this->_ucs4_string_to_ucs4($decoded);
2336
+        case 'ucs4_array': // No break; before this line. Catch case, but do nothing
2337
+            break;
2338
+        default:
2339
+            throw new InvalidArgumentException('Unsupported input format');
2340
+        }
2341
+
2342
+        // No input, no output, what else did you expect?
2343
+        if (empty($decoded)) return '';
2344
+
2345
+        // Anchors for iteration
2346
+        $last_begin = 0;
2347
+        // Output string
2348
+        $output = '';
2349
+
2350
+        foreach ($decoded as $k => $v) {
2351
+            // Make sure to use just the plain dot
2352
+            switch($v) {
2353
+            case 0x3002:
2354
+            case 0xFF0E:
2355
+            case 0xFF61:
2356
+                $decoded[$k] = 0x2E;
2357
+                // It's right, no break here
2358
+                // The codepoints above have to be converted to dots anyway
2359
+
2360
+            // Stumbling across an anchoring character
2361
+            case 0x2E:
2362
+            case 0x2F:
2363
+            case 0x3A:
2364
+            case 0x3F:
2365
+            case 0x40:
2366
+                // Neither email addresses nor URLs allowed in strict mode
2367
+                if ($this->_strict_mode) {
2368
+                    throw new InvalidArgumentException('Neither email addresses nor URLs are allowed in strict mode.');
2369
+                }
2370
+                // Skip first char
2371
+                if ($k) {
2372
+                    $encoded = '';
2373
+                    $encoded = $this->_encode(array_slice($decoded, $last_begin, (($k)-$last_begin)));
2374
+                    if ($encoded) {
2375
+                        $output .= $encoded;
2376
+                    } else {
2377
+                        $output .= $this->_ucs4_to_utf8(array_slice($decoded, $last_begin, (($k)-$last_begin)));
2378
+                    }
2379
+                    $output .= chr($decoded[$k]);
2380
+                }
2381
+                $last_begin = $k + 1;
2382
+            }
2383
+        }
2384
+        // Catch the rest of the string
2385
+        if ($last_begin) {
2386
+            $inp_len = sizeof($decoded);
2387
+            $encoded = '';
2388
+            $encoded = $this->_encode(array_slice($decoded, $last_begin, (($inp_len)-$last_begin)));
2389
+            if ($encoded) {
2390
+                $output .= $encoded;
2391
+            } else {
2392
+                $output .= $this->_ucs4_to_utf8(array_slice($decoded, $last_begin, (($inp_len)-$last_begin)));
2393
+            }
2394
+            return $output;
2395
+        }
2396
+
2397
+        if ($output = $this->_encode($decoded)) {
2398
+            return $output;
2399
+        }
2400
+
2401
+        return $this->_ucs4_to_utf8($decoded);
2402
+    }
2403
+
2404
+    /**
2405
+     * Decode a given ACE domain name.
2406
+     *
2407
+     * @param string $input             Domain name (ACE string)
2408
+     * @param string $one_time_encoding Desired output encoding, see {@link set_parameter}
2409
+     *
2410
+     * @return string                   Decoded Domain name (UTF-8 or UCS-4)
2411
+     * @throws Exception
2412
+     * @access public
2413
+     */
2414
+    public function decode($input, $one_time_encoding = false)
2415
+    {
2416
+        // Optionally set
2417
+        if ($one_time_encoding) {
2418
+            switch ($one_time_encoding) {
2419
+            case 'utf8':
2420
+            case 'ucs4_string':
2421
+            case 'ucs4_array':
2422
+                break;
2423
+            default:
2424
+                throw new InvalidArgumentException('Unknown encoding '.$one_time_encoding);
2425
+            }
2426
+        }
2427
+        // Make sure to drop any newline characters around
2428
+        $input = trim($input);
2429
+
2430
+        // Negotiate input and try to determine, wether it is a plain string,
2431
+        // an email address or something like a complete URL
2432
+        if (strpos($input, '@')) { // Maybe it is an email address
2433
+            // No no in strict mode
2434
+            if ($this->_strict_mode) {
2435
+                throw new InvalidArgumentException('Only simple domain name parts can be handled in strict mode');
2436
+            }
2437
+            list($email_pref, $input) = explode('@', $input, 2);
2438
+            $arr = explode('.', $input);
2439
+            foreach ($arr as $k => $v) {
2440
+                $conv = $this->_decode($v);
2441
+                if ($conv) $arr[$k] = $conv;
2442
+            }
2443
+            $return = $email_pref . '@' . join('.', $arr);
2444
+        } elseif (preg_match('![:\./]!', $input)) { // Or a complete domain name (with or without paths / parameters)
2445
+            // No no in strict mode
2446
+            if ($this->_strict_mode) {
2447
+                throw new InvalidArgumentException('Only simple domain name parts can be handled in strict mode');
2448
+            }
2449
+
2450
+            $parsed = parse_url($input);
2451
+            if (isset($parsed['host'])) {
2452
+                $arr = explode('.', $parsed['host']);
2453
+                foreach ($arr as $k => $v) {
2454
+                    $conv = $this->_decode($v);
2455
+                    if ($conv) $arr[$k] = $conv;
2456
+                }
2457
+                $parsed['host'] = join('.', $arr);
2458
+                if (isset($parsed['scheme'])) {
2459
+                    $parsed['scheme'] .= (strtolower($parsed['scheme']) == 'mailto') ? ':' : '://';
2460
+                }
2461
+                $return = $this->_unparse_url($parsed);
2462
+            } else { // parse_url seems to have failed, try without it
2463
+                $arr = explode('.', $input);
2464
+                foreach ($arr as $k => $v) {
2465
+                    $conv = $this->_decode($v);
2466
+                    if ($conv) $arr[$k] = $conv;
2467
+                }
2468
+                $return = join('.', $arr);
2469
+            }
2470
+        } else { // Otherwise we consider it being a pure domain name string
2471
+            $return = $this->_decode($input);
2472
+        }
2473
+        // The output is UTF-8 by default, other output formats need conversion here
2474
+        // If one time encoding is given, use this, else the objects property
2475
+        switch (($one_time_encoding) ? $one_time_encoding : $this->_api_encoding) {
2476
+        case 'utf8':
2477
+            return $return;
2478
+            break;
2479
+        case 'ucs4_string':
2480
+            return $this->_ucs4_to_ucs4_string($this->_utf8_to_ucs4($return));
2481
+            break;
2482
+        case 'ucs4_array':
2483
+            return $this->_utf8_to_ucs4($return);
2484
+            break;
2485
+        default:
2486
+            throw new InvalidArgumentException('Unsupported output format');
2487
+        }
2488
+    }
2489
+
2490
+
2491
+    // {{{ private
2492
+    /**
2493
+     * Opposite function to parse_url()
2494
+     *
2495
+     * Inspired by code from comments of php.net-documentation for parse_url()
2496
+     *
2497
+     * @param array $parts_arr parts (strings) as returned by parse_url()
2498
+     *
2499
+     * @return string
2500
+     * @access private
2501
+     */
2502
+    private function _unparse_url($parts_arr)
2503
+    {
2504
+        if (!empty($parts_arr['scheme'])) {
2505
+            $ret_url = $parts_arr['scheme'];
2506
+        }
2507
+        if (!empty($parts_arr['user'])) {
2508
+            $ret_url .= $parts_arr['user'];
2509
+            if (!empty($parts_arr['pass'])) {
2510
+                $ret_url .= ':' . $parts_arr['pass'];
2511
+            }
2512
+            $ret_url .= '@';
2513
+        }
2514
+        $ret_url .= $parts_arr['host'];
2515
+        if (!empty($parts_arr['port'])) {
2516
+            $ret_url .= ':' . $parts_arr['port'];
2517
+        }
2518
+        $ret_url .= $parts_arr['path'];
2519
+        if (!empty($parts_arr['query'])) {
2520
+            $ret_url .= '?' . $parts_arr['query'];
2521
+        }
2522
+        if (!empty($parts_arr['fragment'])) {
2523
+            $ret_url .= '#' . $parts_arr['fragment'];
2524
+        }
2525
+        return $ret_url;
2526
+    }
2527
+
2528
+    /**
2529
+     * The actual encoding algorithm.
2530
+     *
2531
+     * @param string $decoded Decoded string which should be encoded
2532
+     *
2533
+     * @return string         Encoded string
2534
+     * @throws Exception
2535
+     * @access private
2536
+     */
2537
+    private function _encode($decoded)
2538
+    {
2539
+        // We cannot encode a domain name containing the Punycode prefix
2540
+        $extract = self::_byteLength($this->_punycode_prefix);
2541
+        $check_pref = $this->_utf8_to_ucs4($this->_punycode_prefix);
2542
+        $check_deco = array_slice($decoded, 0, $extract);
2543
+
2544
+        if ($check_pref == $check_deco) {
2545
+            throw new InvalidArgumentException('This is already a punycode string');
2546
+        }
2547
+
2548
+        // We will not try to encode strings consisting of basic code points only
2549
+        $encodable = false;
2550
+        foreach ($decoded as $k => $v) {
2551
+            if ($v > 0x7a) {
2552
+                $encodable = true;
2553
+                break;
2554
+            }
2555
+        }
2556
+        if (!$encodable) {
2557
+            if ($this->_strict_mode) {
2558
+                throw new InvalidArgumentException('The given string does not contain encodable chars');
2559
+            }
2560
+
2561
+            return false;
2562
+        }
2563
+
2564
+        // Do NAMEPREP
2565
+        $decoded = $this->_nameprep($decoded);
2566
+
2567
+        $deco_len = count($decoded);
2568
+
2569
+        // Empty array
2570
+        if (!$deco_len) {
2571
+            return false;
2572
+        }
2573
+
2574
+        // How many chars have been consumed
2575
+        $codecount = 0;
2576
+
2577
+        // Start with the prefix; copy it to output
2578
+        $encoded = $this->_punycode_prefix;
2579
+
2580
+        $encoded = '';
2581
+        // Copy all basic code points to output
2582
+        for ($i = 0; $i < $deco_len; ++$i) {
2583
+            $test = $decoded[$i];
2584
+            // Will match [0-9a-zA-Z-]
2585
+            if ((0x2F < $test && $test < 0x40)
2586
+                || (0x40 < $test && $test < 0x5B)
2587
+                || (0x60 < $test && $test <= 0x7B)
2588
+                || (0x2D == $test)
2589
+            ) {
2590
+                $encoded .= chr($decoded[$i]);
2591
+                $codecount++;
2592
+            }
2593
+        }
2594
+
2595
+        // All codepoints were basic ones
2596
+        if ($codecount == $deco_len) {
2597
+            return $encoded;
2598
+        }
2599
+
2600
+        // Start with the prefix; copy it to output
2601
+        $encoded = $this->_punycode_prefix . $encoded;
2602
+
2603
+        // If we have basic code points in output, add an hyphen to the end
2604
+        if ($codecount) {
2605
+            $encoded .= '-';
2606
+        }
2607
+
2608
+        // Now find and encode all non-basic code points
2609
+        $is_first  = true;
2610
+        $cur_code  = $this->_initial_n;
2611
+        $bias      = $this->_initial_bias;
2612
+        $delta     = 0;
2613
+
2614
+        while ($codecount < $deco_len) {
2615
+            // Find the smallest code point >= the current code point and
2616
+            // remember the last ouccrence of it in the input
2617
+            for ($i = 0, $next_code = $this->_max_ucs; $i < $deco_len; $i++) {
2618
+                if ($decoded[$i] >= $cur_code && $decoded[$i] <= $next_code) {
2619
+                    $next_code = $decoded[$i];
2620
+                }
2621
+            }
2622
+
2623
+            $delta += ($next_code - $cur_code) * ($codecount + 1);
2624
+            $cur_code = $next_code;
2625
+
2626
+            // Scan input again and encode all characters whose code point is $cur_code
2627
+            for ($i = 0; $i < $deco_len; $i++) {
2628
+                if ($decoded[$i] < $cur_code) {
2629
+                    $delta++;
2630
+                } else if ($decoded[$i] == $cur_code) {
2631
+                    for ($q = $delta, $k = $this->_base; 1; $k += $this->_base) {
2632
+                        $t = ($k <= $bias)?
2633
+                            $this->_tmin :
2634
+                            (($k >= $bias + $this->_tmax)? $this->_tmax : $k - $bias);
2635
+
2636
+                        if ($q < $t) {
2637
+                            break;
2638
+                        }
2639
+
2640
+                        $encoded .= $this->_encodeDigit(ceil($t + (($q - $t) % ($this->_base - $t))));
2641
+                        $q = ($q - $t) / ($this->_base - $t);
2642
+                    }
2643
+
2644
+                    $encoded .= $this->_encodeDigit($q);
2645
+                    $bias = $this->_adapt($delta, $codecount + 1, $is_first);
2646
+                    $codecount++;
2647
+                    $delta = 0;
2648
+                    $is_first = false;
2649
+                }
2650
+            }
2651
+
2652
+            $delta++;
2653
+            $cur_code++;
2654
+        }
2655
+
2656
+        return $encoded;
2657
+    }
2658
+
2659
+    /**
2660
+     * The actual decoding algorithm.
2661
+     *
2662
+     * @param string $encoded Encoded string which should be decoded
2663
+     *
2664
+     * @return string         Decoded string
2665
+     * @throws Exception
2666
+     * @access private
2667
+     */
2668
+    private function _decode($encoded)
2669
+    {
2670
+        // We do need to find the Punycode prefix
2671
+        if (!preg_match('!^' . preg_quote($this->_punycode_prefix, '!') . '!', $encoded)) {
2672
+            return false;
2673
+        }
2674
+
2675
+        $encode_test = preg_replace('!^' . preg_quote($this->_punycode_prefix, '!') . '!', '', $encoded);
2676
+
2677
+        // If nothing left after removing the prefix, it is hopeless
2678
+        if (!$encode_test) {
2679
+            return false;
2680
+        }
2681
+
2682
+        // Find last occurence of the delimiter
2683
+        $delim_pos = strrpos($encoded, '-');
2684
+
2685
+        if ($delim_pos > self::_byteLength($this->_punycode_prefix)) {
2686
+            for ($k = self::_byteLength($this->_punycode_prefix); $k < $delim_pos; ++$k) {
2687
+                $decoded[] = ord($encoded{$k});
2688
+            }
2689
+        } else {
2690
+            $decoded = array();
2691
+        }
2692
+
2693
+        $deco_len = count($decoded);
2694
+        $enco_len = self::_byteLength($encoded);
2695
+
2696
+        // Wandering through the strings; init
2697
+        $is_first = true;
2698
+        $bias     = $this->_initial_bias;
2699
+        $idx      = 0;
2700
+        $char     = $this->_initial_n;
2701
+
2702
+        for ($enco_idx = ($delim_pos)? ($delim_pos + 1) : 0; $enco_idx < $enco_len; ++$deco_len) {
2703
+            for ($old_idx = $idx, $w = 1, $k = $this->_base; 1 ; $k += $this->_base) {
2704
+                $digit = $this->_decodeDigit($encoded{$enco_idx++});
2705
+                $idx += $digit * $w;
2706
+
2707
+                $t = ($k <= $bias) ?
2708
+                    $this->_tmin :
2709
+                    (($k >= $bias + $this->_tmax)? $this->_tmax : ($k - $bias));
2710
+
2711
+                if ($digit < $t) {
2712
+                    break;
2713
+                }
2714
+
2715
+                $w = (int)($w * ($this->_base - $t));
2716
+            }
2717
+
2718
+            $bias      = $this->_adapt($idx - $old_idx, $deco_len + 1, $is_first);
2719
+            $is_first  = false;
2720
+            $char     += (int) ($idx / ($deco_len + 1));
2721
+            $idx      %= ($deco_len + 1);
2722
+
2723
+            if ($deco_len > 0) {
2724
+                // Make room for the decoded char
2725
+                for ($i = $deco_len; $i > $idx; $i--) {
2726
+                    $decoded[$i] = $decoded[($i - 1)];
2727
+                }
2728
+            }
2729
+
2730
+            $decoded[$idx++] = $char;
2731
+        }
2732
+
2733
+        return $this->_ucs4_to_utf8($decoded);
2734
+    }
2735
+
2736
+    /**
2737
+     * Adapt the bias according to the current code point and position.
2738
+     *
2739
+     * @param int     $delta    ...
2740
+     * @param int     $npoints  ...
2741
+     * @param boolean $is_first ...
2742
+     *
2743
+     * @return int
2744
+     * @access private
2745
+     */
2746
+    private function _adapt($delta, $npoints, $is_first)
2747
+    {
2748
+        $delta = (int) ($is_first ? ($delta / $this->_damp) : ($delta / 2));
2749
+        $delta += (int) ($delta / $npoints);
2750
+
2751
+        for ($k = 0; $delta > (($this->_base - $this->_tmin) * $this->_tmax) / 2; $k += $this->_base) {
2752
+            $delta = (int) ($delta / ($this->_base - $this->_tmin));
2753
+        }
2754
+
2755
+        return (int) ($k + ($this->_base - $this->_tmin + 1) * $delta / ($delta + $this->_skew));
2756
+    }
2757
+
2758
+    /**
2759
+     * Encoding a certain digit.
2760
+     *
2761
+     * @param int $d One digit to encode
2762
+     *
2763
+     * @return char  Encoded digit
2764
+     * @access private
2765
+     */
2766
+    private function _encodeDigit($d)
2767
+    {
2768
+        return chr($d + 22 + 75 * ($d < 26));
2769
+    }
2770
+
2771
+    /**
2772
+     * Decode a certain digit.
2773
+     *
2774
+     * @param char $cp One digit (character) to decode
2775
+     *
2776
+     * @return int     Decoded digit
2777
+     * @access private
2778
+     */
2779
+    private function _decodeDigit($cp)
2780
+    {
2781
+        $cp = ord($cp);
2782
+        return ($cp - 48 < 10)? $cp - 22 : (($cp - 65 < 26)? $cp - 65 : (($cp - 97 < 26)? $cp - 97 : $this->_base));
2783
+    }
2784
+
2785
+    /**
2786
+     * Do Nameprep according to RFC3491 and RFC3454.
2787
+     *
2788
+     * @param array $input Unicode Characters
2789
+     *
2790
+     * @return string      Unicode Characters, Nameprep'd
2791
+     * @throws Exception
2792
+     * @access private
2793
+     */
2794
+    private function _nameprep($input)
2795
+    {
2796
+        $output = array();
2797
+
2798
+        // Walking through the input array, performing the required steps on each of
2799
+        // the input chars and putting the result into the output array
2800
+        // While mapping required chars we apply the cannonical ordering
2801
+
2802
+        foreach ($input as $v) {
2803
+            // Map to nothing == skip that code point
2804
+            if (in_array($v, self::$_np_map_nothing)) {
2805
+                continue;
2806
+            }
2807
+
2808
+            // Try to find prohibited input
2809
+            if (in_array($v, self::$_np_prohibit) || in_array($v, self::$_general_prohibited)) {
2810
+                throw new Net_IDNA2_Exception_Nameprep('Prohibited input U+' . sprintf('%08X', $v));
2811
+            }
2812
+
2813
+            foreach (self::$_np_prohibit_ranges as $range) {
2814
+                if ($range[0] <= $v && $v <= $range[1]) {
2815
+                    throw new Net_IDNA2_Exception_Nameprep('Prohibited input U+' . sprintf('%08X', $v));
2816
+                }
2817
+            }
2818
+
2819
+            // Hangul syllable decomposition
2820
+            if (0xAC00 <= $v && $v <= 0xD7AF) {
2821
+                foreach ($this->_hangulDecompose($v) as $out) {
2822
+                    $output[] = $out;
2823
+                }
2824
+            } else if (($this->_version == '2003') && isset(self::$_np_replacemaps[$v])) {
2825
+                // There's a decomposition mapping for that code point
2826
+                // Decompositions only in version 2003 (original) of IDNA
2827
+                foreach ($this->_applyCannonicalOrdering(self::$_np_replacemaps[$v]) as $out) {
2828
+                    $output[] = $out;
2829
+                }
2830
+            } else {
2831
+                $output[] = $v;
2832
+            }
2833
+        }
2834
+
2835
+        // Combine code points
2836
+
2837
+        $last_class   = 0;
2838
+        $last_starter = 0;
2839
+        $out_len      = count($output);
2840
+
2841
+        for ($i = 0; $i < $out_len; ++$i) {
2842
+            $class = $this->_getCombiningClass($output[$i]);
2843
+
2844
+            if ((!$last_class || $last_class != $class) && $class) {
2845
+                // Try to match
2846
+                $seq_len = $i - $last_starter;
2847
+                $out = $this->_combine(array_slice($output, $last_starter, $seq_len));
2848
+
2849
+                // On match: Replace the last starter with the composed character and remove
2850
+                // the now redundant non-starter(s)
2851
+                if ($out) {
2852
+                    $output[$last_starter] = $out;
2853
+
2854
+                    if (count($out) != $seq_len) {
2855
+                        for ($j = $i + 1; $j < $out_len; ++$j) {
2856
+                            $output[$j - 1] = $output[$j];
2857
+                        }
2858
+
2859
+                        unset($output[$out_len]);
2860
+                    }
2861
+
2862
+                    // Rewind the for loop by one, since there can be more possible compositions
2863
+                    $i--;
2864
+                    $out_len--;
2865
+                    $last_class = ($i == $last_starter)? 0 : $this->_getCombiningClass($output[$i - 1]);
2866
+
2867
+                    continue;
2868
+                }
2869
+            }
2870
+
2871
+            // The current class is 0
2872
+            if (!$class) {
2873
+                $last_starter = $i;
2874
+            }
2875
+
2876
+            $last_class = $class;
2877
+        }
2878
+
2879
+        return $output;
2880
+    }
2881
+
2882
+    /**
2883
+     * Decomposes a Hangul syllable
2884
+     * (see http://www.unicode.org/unicode/reports/tr15/#Hangul).
2885
+     *
2886
+     * @param integer $char 32bit UCS4 code point
2887
+     *
2888
+     * @return array        Either Hangul Syllable decomposed or original 32bit
2889
+     *                      value as one value array
2890
+     * @access private
2891
+     */
2892
+    private function _hangulDecompose($char)
2893
+    {
2894
+        $sindex = $char - $this->_sbase;
2895
+
2896
+        if ($sindex < 0 || $sindex >= $this->_scount) {
2897
+            return array($char);
2898
+        }
2899
+
2900
+        $result   = array();
2901
+        $T        = $this->_tbase + $sindex % $this->_tcount;
2902
+        $result[] = (int)($this->_lbase +  $sindex / $this->_ncount);
2903
+        $result[] = (int)($this->_vbase + ($sindex % $this->_ncount) / $this->_tcount);
2904
+
2905
+        if ($T != $this->_tbase) {
2906
+            $result[] = $T;
2907
+        }
2908
+
2909
+        return $result;
2910
+    }
2911
+
2912
+    /**
2913
+     * Ccomposes a Hangul syllable
2914
+     * (see http://www.unicode.org/unicode/reports/tr15/#Hangul).
2915
+     *
2916
+     * @param array $input Decomposed UCS4 sequence
2917
+     *
2918
+     * @return array       UCS4 sequence with syllables composed
2919
+     * @access private
2920
+     */
2921
+    private function _hangulCompose($input)
2922
+    {
2923
+        $inp_len = count($input);
2924
+
2925
+        if (!$inp_len) {
2926
+            return array();
2927
+        }
2928
+
2929
+        $result   = array();
2930
+        $last     = $input[0];
2931
+        $result[] = $last; // copy first char from input to output
2932
+
2933
+        for ($i = 1; $i < $inp_len; ++$i) {
2934
+            $char = $input[$i];
2935
+
2936
+            // Find out, wether two current characters from L and V
2937
+            $lindex = $last - $this->_lbase;
2938
+
2939
+            if (0 <= $lindex && $lindex < $this->_lcount) {
2940
+                $vindex = $char - $this->_vbase;
2941
+
2942
+                if (0 <= $vindex && $vindex < $this->_vcount) {
2943
+                    // create syllable of form LV
2944
+                    $last    = ($this->_sbase + ($lindex * $this->_vcount + $vindex) * $this->_tcount);
2945
+                    $out_off = count($result) - 1;
2946
+                    $result[$out_off] = $last; // reset last
2947
+
2948
+                    // discard char
2949
+                    continue;
2950
+                }
2951
+            }
2952
+
2953
+            // Find out, wether two current characters are LV and T
2954
+            $sindex = $last - $this->_sbase;
2955
+
2956
+            if (0 <= $sindex && $sindex < $this->_scount && ($sindex % $this->_tcount) == 0) {
2957
+                $tindex = $char - $this->_tbase;
2958
+
2959
+                if (0 <= $tindex && $tindex <= $this->_tcount) {
2960
+                    // create syllable of form LVT
2961
+                    $last += $tindex;
2962
+                    $out_off = count($result) - 1;
2963
+                    $result[$out_off] = $last; // reset last
2964
+
2965
+                    // discard char
2966
+                    continue;
2967
+                }
2968
+            }
2969
+
2970
+            // if neither case was true, just add the character
2971
+            $last = $char;
2972
+            $result[] = $char;
2973
+        }
2974
+
2975
+        return $result;
2976
+    }
2977
+
2978
+    /**
2979
+     * Returns the combining class of a certain wide char.
2980
+     *
2981
+     * @param integer $char Wide char to check (32bit integer)
2982
+     *
2983
+     * @return integer      Combining class if found, else 0
2984
+     * @access private
2985
+     */
2986
+    private function _getCombiningClass($char)
2987
+    {
2988
+        return isset(self::$_np_norm_combcls[$char])? self::$_np_norm_combcls[$char] : 0;
2989
+    }
2990
+
2991
+    /**
2992
+     * Apllies the cannonical ordering of a decomposed UCS4 sequence.
2993
+     *
2994
+     * @param array $input Decomposed UCS4 sequence
2995
+     *
2996
+     * @return array       Ordered USC4 sequence
2997
+     * @access private
2998
+     */
2999
+    private function _applyCannonicalOrdering($input)
3000
+    {
3001
+        $swap = true;
3002
+        $size = count($input);
3003
+
3004
+        while ($swap) {
3005
+            $swap = false;
3006
+            $last = $this->_getCombiningClass($input[0]);
3007
+
3008
+            for ($i = 0; $i < $size - 1; ++$i) {
3009
+                $next = $this->_getCombiningClass($input[$i + 1]);
3010
+
3011
+                if ($next != 0 && $last > $next) {
3012
+                    // Move item leftward until it fits
3013
+                    for ($j = $i + 1; $j > 0; --$j) {
3014
+                        if ($this->_getCombiningClass($input[$j - 1]) <= $next) {
3015
+                            break;
3016
+                        }
3017
+
3018
+                        $t = $input[$j];
3019
+                        $input[$j] = $input[$j - 1];
3020
+                        $input[$j - 1] = $t;
3021
+                        $swap = 1;
3022
+                    }
3023
+
3024
+                    // Reentering the loop looking at the old character again
3025
+                    $next = $last;
3026
+                }
3027
+
3028
+                $last = $next;
3029
+            }
3030
+        }
3031
+
3032
+        return $input;
3033
+    }
3034
+
3035
+    /**
3036
+     * Do composition of a sequence of starter and non-starter.
3037
+     *
3038
+     * @param array $input UCS4 Decomposed sequence
3039
+     *
3040
+     * @return array       Ordered USC4 sequence
3041
+     * @access private
3042
+     */
3043
+    private function _combine($input)
3044
+    {
3045
+        $inp_len = count($input);
3046
+
3047
+        // Is it a Hangul syllable?
3048
+        if (1 != $inp_len) {
3049
+            $hangul = $this->_hangulCompose($input);
3050
+
3051
+            // This place is probably wrong
3052
+            if (count($hangul) != $inp_len) {
3053
+                return $hangul;
3054
+            }
3055
+        }
3056
+
3057
+        foreach (self::$_np_replacemaps as $np_src => $np_target) {
3058
+            if ($np_target[0] != $input[0]) {
3059
+                continue;
3060
+            }
3061
+
3062
+            if (count($np_target) != $inp_len) {
3063
+                continue;
3064
+            }
3065
+
3066
+            $hit = false;
3067
+
3068
+            foreach ($input as $k2 => $v2) {
3069
+                if ($v2 == $np_target[$k2]) {
3070
+                    $hit = true;
3071
+                } else {
3072
+                    $hit = false;
3073
+                    break;
3074
+                }
3075
+            }
3076
+
3077
+            if ($hit) {
3078
+                return $np_src;
3079
+            }
3080
+        }
3081
+
3082
+        return false;
3083
+    }
3084
+
3085
+    /**
3086
+     * This converts an UTF-8 encoded string to its UCS-4 (array) representation
3087
+     * By talking about UCS-4 we mean arrays of 32bit integers representing
3088
+     * each of the "chars". This is due to PHP not being able to handle strings with
3089
+     * bit depth different from 8. This applies to the reverse method _ucs4_to_utf8(), too.
3090
+     * The following UTF-8 encodings are supported:
3091
+     *
3092
+     * bytes bits  representation
3093
+     * 1        7  0xxxxxxx
3094
+     * 2       11  110xxxxx 10xxxxxx
3095
+     * 3       16  1110xxxx 10xxxxxx 10xxxxxx
3096
+     * 4       21  11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
3097
+     * 5       26  111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
3098
+     * 6       31  1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
3099
+     *
3100
+     * Each x represents a bit that can be used to store character data.
3101
+     *
3102
+     * @param string $input utf8-encoded string
3103
+     *
3104
+     * @return array        ucs4-encoded array
3105
+     * @throws Exception
3106
+     * @access private
3107
+     */
3108
+    private function _utf8_to_ucs4($input)
3109
+    {
3110
+        $output = array();
3111
+        $out_len = 0;
3112
+        $inp_len = self::_byteLength($input, '8bit');
3113
+        $mode = 'next';
3114
+        $test = 'none';
3115
+        for ($k = 0; $k < $inp_len; ++$k) {
3116
+            $v = ord($input{$k}); // Extract byte from input string
3117
+
3118
+            if ($v < 128) { // We found an ASCII char - put into stirng as is
3119
+                $output[$out_len] = $v;
3120
+                ++$out_len;
3121
+                if ('add' == $mode) {
3122
+                    throw new UnexpectedValueException('Conversion from UTF-8 to UCS-4 failed: malformed input at byte '.$k);
3123
+                }
3124
+                continue;
3125
+            }
3126
+            if ('next' == $mode) { // Try to find the next start byte; determine the width of the Unicode char
3127
+                $start_byte = $v;
3128
+                $mode = 'add';
3129
+                $test = 'range';
3130
+                if ($v >> 5 == 6) { // &110xxxxx 10xxxxx
3131
+                    $next_byte = 0; // Tells, how many times subsequent bitmasks must rotate 6bits to the left
3132
+                    $v = ($v - 192) << 6;
3133
+                } elseif ($v >> 4 == 14) { // &1110xxxx 10xxxxxx 10xxxxxx
3134
+                    $next_byte = 1;
3135
+                    $v = ($v - 224) << 12;
3136
+                } elseif ($v >> 3 == 30) { // &11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
3137
+                    $next_byte = 2;
3138
+                    $v = ($v - 240) << 18;
3139
+                } elseif ($v >> 2 == 62) { // &111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
3140
+                    $next_byte = 3;
3141
+                    $v = ($v - 248) << 24;
3142
+                } elseif ($v >> 1 == 126) { // &1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
3143
+                    $next_byte = 4;
3144
+                    $v = ($v - 252) << 30;
3145
+                } else {
3146
+                    throw new UnexpectedValueException('This might be UTF-8, but I don\'t understand it at byte '.$k);
3147
+                }
3148
+                if ('add' == $mode) {
3149
+                    $output[$out_len] = (int) $v;
3150
+                    ++$out_len;
3151
+                    continue;
3152
+                }
3153
+            }
3154
+            if ('add' == $mode) {
3155
+                if (!$this->_allow_overlong && $test == 'range') {
3156
+                    $test = 'none';
3157
+                    if (($v < 0xA0 && $start_byte == 0xE0) || ($v < 0x90 && $start_byte == 0xF0) || ($v > 0x8F && $start_byte == 0xF4)) {
3158
+                        throw new OutOfRangeException('Bogus UTF-8 character detected (out of legal range) at byte '.$k);
3159
+                    }
3160
+                }
3161
+                if ($v >> 6 == 2) { // Bit mask must be 10xxxxxx
3162
+                    $v = ($v - 128) << ($next_byte * 6);
3163
+                    $output[($out_len - 1)] += $v;
3164
+                    --$next_byte;
3165
+                } else {
3166
+                    throw new UnexpectedValueException('Conversion from UTF-8 to UCS-4 failed: malformed input at byte '.$k);
3167
+                }
3168
+                if ($next_byte < 0) {
3169
+                    $mode = 'next';
3170
+                }
3171
+            }
3172
+        } // for
3173
+        return $output;
3174
+    }
3175
+
3176
+    /**
3177
+     * Convert UCS-4 array into UTF-8 string
3178
+     *
3179
+     * @param array $input ucs4-encoded array
3180
+     *
3181
+     * @return string      utf8-encoded string
3182
+     * @throws Exception
3183
+     * @access private
3184
+     */
3185
+    private function _ucs4_to_utf8($input)
3186
+    {
3187
+        $output = '';
3188
+
3189
+        foreach ($input as $v) {
3190
+            // $v = ord($v);
3191
+
3192
+            if ($v < 128) {
3193
+                // 7bit are transferred literally
3194
+                $output .= chr($v);
3195
+            } else if ($v < 1 << 11) {
3196
+                // 2 bytes
3197
+                $output .= chr(192 + ($v >> 6))
3198
+                    . chr(128 + ($v & 63));
3199
+            } else if ($v < 1 << 16) {
3200
+                // 3 bytes
3201
+                $output .= chr(224 + ($v >> 12))
3202
+                    . chr(128 + (($v >> 6) & 63))
3203
+                    . chr(128 + ($v & 63));
3204
+            } else if ($v < 1 << 21) {
3205
+                // 4 bytes
3206
+                $output .= chr(240 + ($v >> 18))
3207
+                    . chr(128 + (($v >> 12) & 63))
3208
+                    . chr(128 + (($v >>  6) & 63))
3209
+                    . chr(128 + ($v & 63));
3210
+            } else if ($v < 1 << 26) {
3211
+                // 5 bytes
3212
+                $output .= chr(248 + ($v >> 24))
3213
+                    . chr(128 + (($v >> 18) & 63))
3214
+                    . chr(128 + (($v >> 12) & 63))
3215
+                    . chr(128 + (($v >>  6) & 63))
3216
+                    . chr(128 + ($v & 63));
3217
+            } else if ($v < 1 << 31) {
3218
+                // 6 bytes
3219
+                $output .= chr(252 + ($v >> 30))
3220
+                    . chr(128 + (($v >> 24) & 63))
3221
+                    . chr(128 + (($v >> 18) & 63))
3222
+                    . chr(128 + (($v >> 12) & 63))
3223
+                    . chr(128 + (($v >>  6) & 63))
3224
+                    . chr(128 + ($v & 63));
3225
+            } else {
3226
+                throw new UnexpectedValueException('Conversion from UCS-4 to UTF-8 failed: malformed input');
3227
+            }
3228
+        }
3229
+
3230
+        return $output;
3231
+    }
3232
+
3233
+    /**
3234
+     * Convert UCS-4 array into UCS-4 string
3235
+     *
3236
+     * @param array $input ucs4-encoded array
3237
+     *
3238
+     * @return string      ucs4-encoded string
3239
+     * @throws Exception
3240
+     * @access private
3241
+     */
3242
+    private function _ucs4_to_ucs4_string($input)
3243
+    {
3244
+        $output = '';
3245
+        // Take array values and split output to 4 bytes per value
3246
+        // The bit mask is 255, which reads &11111111
3247
+        foreach ($input as $v) {
3248
+            $output .= ($v & (255 << 24) >> 24) . ($v & (255 << 16) >> 16) . ($v & (255 << 8) >> 8) . ($v & 255);
3249
+        }
3250
+        return $output;
3251
+    }
3252
+
3253
+    /**
3254
+     * Convert UCS-4 string into UCS-4 array
3255
+     *
3256
+     * @param string $input ucs4-encoded string
3257
+     *
3258
+     * @return array        ucs4-encoded array
3259
+     * @throws InvalidArgumentException
3260
+     * @access private
3261
+     */
3262
+    private function _ucs4_string_to_ucs4($input)
3263
+    {
3264
+        $output = array();
3265
+
3266
+        $inp_len = self::_byteLength($input);
3267
+        // Input length must be dividable by 4
3268
+        if ($inp_len % 4) {
3269
+            throw new InvalidArgumentException('Input UCS4 string is broken');
3270
+        }
3271
+
3272
+        // Empty input - return empty output
3273
+        if (!$inp_len) {
3274
+            return $output;
3275
+        }
3276
+
3277
+        for ($i = 0, $out_len = -1; $i < $inp_len; ++$i) {
3278
+            // Increment output position every 4 input bytes
3279
+            if (!$i % 4) {
3280
+                $out_len++;
3281
+                $output[$out_len] = 0;
3282
+            }
3283
+            $output[$out_len] += ord($input{$i}) << (8 * (3 - ($i % 4) ) );
3284
+        }
3285
+        return $output;
3286
+    }
3287
+
3288
+    /**
3289
+     * Echo hex representation of UCS4 sequence.
3290
+     *
3291
+     * @param array   $input       UCS4 sequence
3292
+     * @param boolean $include_bit Include bitmask in output
3293
+     *
3294
+     * @return void
3295
+     * @static
3296
+     * @access private
3297
+     */
3298
+    private static function _showHex($input, $include_bit = false)
3299
+    {
3300
+        foreach ($input as $k => $v) {
3301
+            echo '[', $k, '] => ', sprintf('%X', $v);
3302
+
3303
+            if ($include_bit) {
3304
+                echo ' (', Net_IDNA2::_showBitmask($v), ')';
3305
+            }
3306
+
3307
+            echo "\n";
3308
+        }
3309
+    }
3310
+
3311
+    /**
3312
+     * Gives you a bit representation of given Byte (8 bits), Word (16 bits) or DWord (32 bits)
3313
+     * Output width is automagically determined
3314
+     *
3315
+     * @param int $octet ...
3316
+     *
3317
+     * @return string    Bitmask-representation
3318
+     * @static
3319
+     * @access private
3320
+     */
3321
+    private static function _showBitmask($octet)
3322
+    {
3323
+        if ($octet >= (1 << 16)) {
3324
+            $w = 31;
3325
+        } else if ($octet >= (1 << 8)) {
3326
+            $w = 15;
3327
+        } else {
3328
+            $w = 7;
3329
+        }
3330
+
3331
+        $return = '';
3332
+
3333
+        for ($i = $w; $i > -1; $i--) {
3334
+            $return .= ($octet & (1 << $i))? '1' : '0';
3335
+        }
3336
+
3337
+        return $return;
3338
+    }
3339
+
3340
+    /**
3341
+     * Gets the length of a string in bytes even if mbstring function
3342
+     * overloading is turned on
3343
+     *
3344
+     * @param string $string the string for which to get the length.
3345
+     *
3346
+     * @return integer the length of the string in bytes.
3347
+     *
3348
+     * @see Net_IDNA2::$_mb_string_overload
3349
+     */
3350
+    private static function _byteLength($string)
3351
+    {
3352
+        if (self::$_mb_string_overload) {
3353
+            return mb_strlen($string, '8bit');
3354
+        }
3355
+        return strlen((binary)$string);
3356
+    }
3357
+
3358
+    // }}}}
3359
+
3360
+    // {{{ factory
3361
+    /**
3362
+     * Attempts to return a concrete IDNA instance for either php4 or php5.
3363
+     *
3364
+     * @param array $params Set of paramaters
3365
+     *
3366
+     * @return Net_IDNA2
3367
+     * @access public
3368
+     */
3369
+    function getInstance($params = array())
3370
+    {
3371
+        return new Net_IDNA2($params);
3372
+    }
3373
+    // }}}
3374
+
3375
+    // {{{ singleton
3376
+    /**
3377
+     * Attempts to return a concrete IDNA instance for either php4 or php5,
3378
+     * only creating a new instance if no IDNA instance with the same
3379
+     * parameters currently exists.
3380
+     *
3381
+     * @param array $params Set of paramaters
3382
+     *
3383
+     * @return object Net_IDNA2
3384
+     * @access public
3385
+     */
3386
+    function singleton($params = array())
3387
+    {
3388
+        static $instances;
3389
+        if (!isset($instances)) {
3390
+            $instances = array();
3391
+        }
3392
+
3393
+        $signature = serialize($params);
3394
+        if (!isset($instances[$signature])) {
3395
+            $instances[$signature] = Net_IDNA2::getInstance($params);
3396
+        }
3397
+
3398
+        return $instances[$signature];
3399
+    }
3400
+    // }}}
3401
+}
3402
+
3403
+?>
3404
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Net/IDNA2/Exception Added
2
 
1
+(directory)
2
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Net/IDNA2/Exception.php Added
6
 
1
@@ -0,0 +1,4 @@
2
+<?php
3
+class Net_IDNA2_Exception extends Exception
4
+{
5
+}
6
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Net/IDNA2/Exception/Nameprep.php Added
8
 
1
@@ -0,0 +1,6 @@
2
+<?php
3
+require_once 'Net/IDNA2/Exception.php';
4
+
5
+class Net_IDNA2_Exception_Nameprep extends Net_IDNA2_Exception
6
+{
7
+}
8
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Net/SMTP.php Added
1344
 
1
@@ -0,0 +1,1342 @@
2
+<?php
3
+/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */
4
+// +----------------------------------------------------------------------+
5
+// | PHP Version 4                                                        |
6
+// +----------------------------------------------------------------------+
7
+// | Copyright (c) 1997-2003 The PHP Group                                |
8
+// +----------------------------------------------------------------------+
9
+// | This source file is subject to version 2.02 of the PHP license,      |
10
+// | that is bundled with this package in the file LICENSE, and is        |
11
+// | available at through the world-wide-web at                           |
12
+// | http://www.php.net/license/2_02.txt.                                 |
13
+// | If you did not receive a copy of the PHP license and are unable to   |
14
+// | obtain it through the world-wide-web, please send a note to          |
15
+// | license@php.net so we can mail you a copy immediately.               |
16
+// +----------------------------------------------------------------------+
17
+// | Authors: Chuck Hagenbuch <chuck@horde.org>                           |
18
+// |          Jon Parise <jon@php.net>                                    |
19
+// |          Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar>      |
20
+// +----------------------------------------------------------------------+
21
+//
22
+// $Id$
23
+
24
+require_once 'PEAR.php';
25
+require_once 'Net/Socket.php';
26
+
27
+/**
28
+ * Provides an implementation of the SMTP protocol using PEAR's
29
+ * Net_Socket:: class.
30
+ *
31
+ * @package Net_SMTP
32
+ * @author  Chuck Hagenbuch <chuck@horde.org>
33
+ * @author  Jon Parise <jon@php.net>
34
+ * @author  Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar>
35
+ *
36
+ * @example basic.php   A basic implementation of the Net_SMTP package.
37
+ */
38
+class Net_SMTP
39
+{
40
+    /**
41
+     * The server to connect to.
42
+     * @var string
43
+     * @access public
44
+     */
45
+    var $host = 'localhost';
46
+
47
+    /**
48
+     * The port to connect to.
49
+     * @var int
50
+     * @access public
51
+     */
52
+    var $port = 25;
53
+
54
+    /**
55
+     * The value to give when sending EHLO or HELO.
56
+     * @var string
57
+     * @access public
58
+     */
59
+    var $localhost = 'localhost';
60
+
61
+    /**
62
+     * List of supported authentication methods, in preferential order.
63
+     * @var array
64
+     * @access public
65
+     */
66
+    var $auth_methods = array();
67
+
68
+    /**
69
+     * Use SMTP command pipelining (specified in RFC 2920) if the SMTP
70
+     * server supports it.
71
+     *
72
+     * When pipeling is enabled, rcptTo(), mailFrom(), sendFrom(),
73
+     * somlFrom() and samlFrom() do not wait for a response from the
74
+     * SMTP server but return immediately.
75
+     *
76
+     * @var bool
77
+     * @access public
78
+     */
79
+    var $pipelining = false;
80
+
81
+    /**
82
+     * Number of pipelined commands.
83
+     * @var int
84
+     * @access private
85
+     */
86
+    var $_pipelined_commands = 0;
87
+
88
+    /**
89
+     * Should debugging output be enabled?
90
+     * @var boolean
91
+     * @access private
92
+     */
93
+    var $_debug = false;
94
+
95
+    /**
96
+     * Debug output handler.
97
+     * @var callback
98
+     * @access private
99
+     */
100
+    var $_debug_handler = null;
101
+
102
+    /**
103
+     * The socket resource being used to connect to the SMTP server.
104
+     * @var resource
105
+     * @access private
106
+     */
107
+    var $_socket = null;
108
+
109
+    /**
110
+     * Array of socket options that will be passed to Net_Socket::connect().
111
+     * @see stream_context_create()
112
+     * @var array
113
+     * @access private
114
+     */
115
+    var $_socket_options = null;
116
+
117
+    /**
118
+     * The socket I/O timeout value in seconds.
119
+     * @var int
120
+     * @access private
121
+     */
122
+    var $_timeout = 0;
123
+
124
+    /**
125
+     * The most recent server response code.
126
+     * @var int
127
+     * @access private
128
+     */
129
+    var $_code = -1;
130
+
131
+    /**
132
+     * The most recent server response arguments.
133
+     * @var array
134
+     * @access private
135
+     */
136
+    var $_arguments = array();
137
+
138
+    /**
139
+     * Stores the SMTP server's greeting string.
140
+     * @var string
141
+     * @access private
142
+     */
143
+    var $_greeting = null;
144
+
145
+    /**
146
+     * Stores detected features of the SMTP server.
147
+     * @var array
148
+     * @access private
149
+     */
150
+    var $_esmtp = array();
151
+
152
+    /**
153
+     * Instantiates a new Net_SMTP object, overriding any defaults
154
+     * with parameters that are passed in.
155
+     *
156
+     * If you have SSL support in PHP, you can connect to a server
157
+     * over SSL using an 'ssl://' prefix:
158
+     *
159
+     *   // 465 is a common smtps port.
160
+     *   $smtp = new Net_SMTP('ssl://mail.host.com', 465);
161
+     *   $smtp->connect();
162
+     *
163
+     * @param string  $host       The server to connect to.
164
+     * @param integer $port       The port to connect to.
165
+     * @param string  $localhost  The value to give when sending EHLO or HELO.
166
+     * @param boolean $pipeling   Use SMTP command pipelining
167
+     * @param integer $timeout    Socket I/O timeout in seconds.
168
+     * @param array   $socket_options Socket stream_context_create() options.
169
+     *
170
+     * @access  public
171
+     * @since   1.0
172
+     */
173
+    function Net_SMTP($host = null, $port = null, $localhost = null,
174
+        $pipelining = false, $timeout = 0, $socket_options = null)
175
+    {
176
+        if (isset($host)) {
177
+            $this->host = $host;
178
+        }
179
+        if (isset($port)) {
180
+            $this->port = $port;
181
+        }
182
+        if (isset($localhost)) {
183
+            $this->localhost = $localhost;
184
+        }
185
+        $this->pipelining = $pipelining;
186
+
187
+        $this->_socket = new Net_Socket();
188
+        $this->_socket_options = $socket_options;
189
+        $this->_timeout = $timeout;
190
+
191
+        /* Include the Auth_SASL package.  If the package is available, we 
192
+         * enable the authentication methods that depend upon it. */
193
+        if ((@include_once 'Auth/SASL.php') === true) {
194
+            $this->setAuthMethod('CRAM-MD5', array($this, '_authCram_MD5'));
195
+            $this->setAuthMethod('DIGEST-MD5', array($this, '_authDigest_MD5'));
196
+        }
197
+
198
+        /* These standard authentication methods are always available. */
199
+        $this->setAuthMethod('LOGIN', array($this, '_authLogin'), false);
200
+        $this->setAuthMethod('PLAIN', array($this, '_authPlain'), false);
201
+    }
202
+
203
+    /**
204
+     * Set the socket I/O timeout value in seconds plus microseconds.
205
+     *
206
+     * @param   integer $seconds        Timeout value in seconds.
207
+     * @param   integer $microseconds   Additional value in microseconds.
208
+     *
209
+     * @access  public
210
+     * @since   1.5.0
211
+     */
212
+    function setTimeout($seconds, $microseconds = 0) {
213
+        return $this->_socket->setTimeout($seconds, $microseconds);
214
+    }
215
+
216
+    /**
217
+     * Set the value of the debugging flag.
218
+     *
219
+     * @param   boolean $debug      New value for the debugging flag.
220
+     *
221
+     * @access  public
222
+     * @since   1.1.0
223
+     */
224
+    function setDebug($debug, $handler = null)
225
+    {
226
+        $this->_debug = $debug;
227
+        $this->_debug_handler = $handler;
228
+    }
229
+
230
+    /**
231
+     * Write the given debug text to the current debug output handler.
232
+     *
233
+     * @param   string  $message    Debug mesage text.
234
+     *
235
+     * @access  private
236
+     * @since   1.3.3
237
+     */
238
+    function _debug($message)
239
+    {
240
+        if ($this->_debug) {
241
+            if ($this->_debug_handler) {
242
+                call_user_func_array($this->_debug_handler,
243
+                                     array(&$this, $message));
244
+            } else {
245
+                echo "DEBUG: $message\n";
246
+            }
247
+        }
248
+    }
249
+
250
+    /**
251
+     * Send the given string of data to the server.
252
+     *
253
+     * @param   string  $data       The string of data to send.
254
+     *
255
+     * @return  mixed   The number of bytes that were actually written,
256
+     *                  or a PEAR_Error object on failure.
257
+     *
258
+     * @access  private
259
+     * @since   1.1.0
260
+     */
261
+    function _send($data)
262
+    {
263
+        $this->_debug("Send: $data");
264
+
265
+        $result = $this->_socket->write($data);
266
+        if (!$result || PEAR::isError($result)) {
267
+            $msg = ($result) ? $result->getMessage() : "unknown error";
268
+            return PEAR::raiseError("Failed to write to socket: $msg",
269
+                                    null, PEAR_ERROR_RETURN);
270
+        }
271
+
272
+        return $result;
273
+    }
274
+
275
+    /**
276
+     * Send a command to the server with an optional string of
277
+     * arguments.  A carriage return / linefeed (CRLF) sequence will
278
+     * be appended to each command string before it is sent to the
279
+     * SMTP server - an error will be thrown if the command string
280
+     * already contains any newline characters. Use _send() for
281
+     * commands that must contain newlines.
282
+     *
283
+     * @param   string  $command    The SMTP command to send to the server.
284
+     * @param   string  $args       A string of optional arguments to append
285
+     *                              to the command.
286
+     *
287
+     * @return  mixed   The result of the _send() call.
288
+     *
289
+     * @access  private
290
+     * @since   1.1.0
291
+     */
292
+    function _put($command, $args = '')
293
+    {
294
+        if (!empty($args)) {
295
+            $command .= ' ' . $args;
296
+        }
297
+
298
+        if (strcspn($command, "\r\n") !== strlen($command)) {
299
+            return PEAR::raiseError('Commands cannot contain newlines',
300
+                                    null, PEAR_ERROR_RETURN);
301
+        }
302
+
303
+        return $this->_send($command . "\r\n");
304
+    }
305
+
306
+    /**
307
+     * Read a reply from the SMTP server.  The reply consists of a response
308
+     * code and a response message.
309
+     *
310
+     * @param   mixed   $valid      The set of valid response codes.  These
311
+     *                              may be specified as an array of integer
312
+     *                              values or as a single integer value.
313
+     * @param   bool    $later      Do not parse the response now, but wait
314
+     *                              until the last command in the pipelined
315
+     *                              command group
316
+     *
317
+     * @return  mixed   True if the server returned a valid response code or
318
+     *                  a PEAR_Error object is an error condition is reached.
319
+     *
320
+     * @access  private
321
+     * @since   1.1.0
322
+     *
323
+     * @see     getResponse
324
+     */
325
+    function _parseResponse($valid, $later = false)
326
+    {
327
+        $this->_code = -1;
328
+        $this->_arguments = array();
329
+
330
+        if ($later) {
331
+            $this->_pipelined_commands++;
332
+            return true;
333
+        }
334
+
335
+        for ($i = 0; $i <= $this->_pipelined_commands; $i++) {
336
+            while ($line = $this->_socket->readLine()) {
337
+                $this->_debug("Recv: $line");
338
+
339
+                /* If we receive an empty line, the connection was closed. */
340
+                if (empty($line)) {
341
+                    $this->disconnect();
342
+                    return PEAR::raiseError('Connection was closed',
343
+                                            null, PEAR_ERROR_RETURN);
344
+                }
345
+
346
+                /* Read the code and store the rest in the arguments array. */
347
+                $code = substr($line, 0, 3);
348
+                $this->_arguments[] = trim(substr($line, 4));
349
+
350
+                /* Check the syntax of the response code. */
351
+                if (is_numeric($code)) {
352
+                    $this->_code = (int)$code;
353
+                } else {
354
+                    $this->_code = -1;
355
+                    break;
356
+                }
357
+
358
+                /* If this is not a multiline response, we're done. */
359
+                if (substr($line, 3, 1) != '-') {
360
+                    break;
361
+                }
362
+            }
363
+        }
364
+
365
+        $this->_pipelined_commands = 0;
366
+
367
+        /* Compare the server's response code with the valid code/codes. */
368
+        if (is_int($valid) && ($this->_code === $valid)) {
369
+            return true;
370
+        } elseif (is_array($valid) && in_array($this->_code, $valid, true)) {
371
+            return true;
372
+        }
373
+
374
+        return PEAR::raiseError('Invalid response code received from server',
375
+                                $this->_code, PEAR_ERROR_RETURN);
376
+    }
377
+
378
+    /**
379
+     * Issue an SMTP command and verify its response.
380
+     *
381
+     * @param   string  $command    The SMTP command string or data.
382
+     * @param   mixed   $valid      The set of valid response codes.  These
383
+     *                              may be specified as an array of integer
384
+     *                              values or as a single integer value.
385
+     *
386
+     * @return  mixed   True on success or a PEAR_Error object on failure.
387
+     *
388
+     * @access  public
389
+     * @since   1.6.0
390
+     */
391
+    function command($command, $valid)
392
+    {
393
+        if (PEAR::isError($error = $this->_put($command))) {
394
+            return $error;
395
+        }
396
+        if (PEAR::isError($error = $this->_parseResponse($valid))) {
397
+            return $error;
398
+        }
399
+
400
+        return true;
401
+    }
402
+
403
+    /**
404
+     * Return a 2-tuple containing the last response from the SMTP server.
405
+     *
406
+     * @return  array   A two-element array: the first element contains the
407
+     *                  response code as an integer and the second element
408
+     *                  contains the response's arguments as a string.
409
+     *
410
+     * @access  public
411
+     * @since   1.1.0
412
+     */
413
+    function getResponse()
414
+    {
415
+        return array($this->_code, join("\n", $this->_arguments));
416
+    }
417
+
418
+    /**
419
+     * Return the SMTP server's greeting string.
420
+     *
421
+     * @return  string  A string containing the greeting string, or null if a 
422
+     *                  greeting has not been received.
423
+     *
424
+     * @access  public
425
+     * @since   1.3.3
426
+     */
427
+    function getGreeting()
428
+    {
429
+        return $this->_greeting;
430
+    }
431
+
432
+    /**
433
+     * Attempt to connect to the SMTP server.
434
+     *
435
+     * @param   int     $timeout    The timeout value (in seconds) for the
436
+     *                              socket connection attempt.
437
+     * @param   bool    $persistent Should a persistent socket connection
438
+     *                              be used?
439
+     *
440
+     * @return mixed Returns a PEAR_Error with an error message on any
441
+     *               kind of failure, or true on success.
442
+     * @access public
443
+     * @since  1.0
444
+     */
445
+    function connect($timeout = null, $persistent = false)
446
+    {
447
+        $this->_greeting = null;
448
+        $result = $this->_socket->connect($this->host, $this->port,
449
+                                          $persistent, $timeout,
450
+                                          $this->_socket_options);
451
+        if (PEAR::isError($result)) {
452
+            return PEAR::raiseError('Failed to connect socket: ' .
453
+                                    $result->getMessage());
454
+        }
455
+
456
+        /*
457
+         * Now that we're connected, reset the socket's timeout value for 
458
+         * future I/O operations.  This allows us to have different socket 
459
+         * timeout values for the initial connection (our $timeout parameter) 
460
+         * and all other socket operations.
461
+         */
462
+        if ($this->_timeout > 0) {
463
+            if (PEAR::isError($error = $this->setTimeout($this->_timeout))) {
464
+                return $error;
465
+            }
466
+        }
467
+
468
+        if (PEAR::isError($error = $this->_parseResponse(220))) {
469
+            return $error;
470
+        }
471
+
472
+        /* Extract and store a copy of the server's greeting string. */
473
+        list(, $this->_greeting) = $this->getResponse();
474
+
475
+        if (PEAR::isError($error = $this->_negotiate())) {
476
+            return $error;
477
+        }
478
+
479
+        return true;
480
+    }
481
+
482
+    /**
483
+     * Attempt to disconnect from the SMTP server.
484
+     *
485
+     * @return mixed Returns a PEAR_Error with an error message on any
486
+     *               kind of failure, or true on success.
487
+     * @access public
488
+     * @since  1.0
489
+     */
490
+    function disconnect()
491
+    {
492
+        if (PEAR::isError($error = $this->_put('QUIT'))) {
493
+            return $error;
494
+        }
495
+        if (PEAR::isError($error = $this->_parseResponse(221))) {
496
+            return $error;
497
+        }
498
+        if (PEAR::isError($error = $this->_socket->disconnect())) {
499
+            return PEAR::raiseError('Failed to disconnect socket: ' .
500
+                                    $error->getMessage());
501
+        }
502
+
503
+        return true;
504
+    }
505
+
506
+    /**
507
+     * Attempt to send the EHLO command and obtain a list of ESMTP
508
+     * extensions available, and failing that just send HELO.
509
+     *
510
+     * @return mixed Returns a PEAR_Error with an error message on any
511
+     *               kind of failure, or true on success.
512
+     *
513
+     * @access private
514
+     * @since  1.1.0
515
+     */
516
+    function _negotiate()
517
+    {
518
+        if (PEAR::isError($error = $this->_put('EHLO', $this->localhost))) {
519
+            return $error;
520
+        }
521
+
522
+        if (PEAR::isError($this->_parseResponse(250))) {
523
+            /* If we receive a 503 response, we're already authenticated. */
524
+            if ($this->_code === 503) {
525
+                return true;
526
+            }
527
+
528
+            /* If the EHLO failed, try the simpler HELO command. */
529
+            if (PEAR::isError($error = $this->_put('HELO', $this->localhost))) {
530
+                return $error;
531
+            }
532
+            if (PEAR::isError($this->_parseResponse(250))) {
533
+                return PEAR::raiseError('HELO was not accepted: ', $this->_code,
534
+                                        PEAR_ERROR_RETURN);
535
+            }
536
+
537
+            return true;
538
+        }
539
+
540
+        foreach ($this->_arguments as $argument) {
541
+            $verb = strtok($argument, ' ');
542
+            $arguments = substr($argument, strlen($verb) + 1,
543
+                                strlen($argument) - strlen($verb) - 1);
544
+            $this->_esmtp[$verb] = $arguments;
545
+        }
546
+
547
+        if (!isset($this->_esmtp['PIPELINING'])) {
548
+            $this->pipelining = false;
549
+        }
550
+
551
+        return true;
552
+    }
553
+
554
+    /**
555
+     * Returns the name of the best authentication method that the server
556
+     * has advertised.
557
+     *
558
+     * @return mixed    Returns a string containing the name of the best
559
+     *                  supported authentication method or a PEAR_Error object
560
+     *                  if a failure condition is encountered.
561
+     * @access private
562
+     * @since  1.1.0
563
+     */
564
+    function _getBestAuthMethod()
565
+    {
566
+        $available_methods = explode(' ', $this->_esmtp['AUTH']);
567
+
568
+        foreach ($this->auth_methods as $method => $callback) {
569
+            if (in_array($method, $available_methods)) {
570
+                return $method;
571
+            }
572
+        }
573
+
574
+        return PEAR::raiseError('No supported authentication methods',
575
+                                null, PEAR_ERROR_RETURN);
576
+    }
577
+
578
+    /**
579
+     * Attempt to do SMTP authentication.
580
+     *
581
+     * @param string The userid to authenticate as.
582
+     * @param string The password to authenticate with.
583
+     * @param string The requested authentication method.  If none is
584
+     *               specified, the best supported method will be used.
585
+     * @param bool   Flag indicating whether or not TLS should be attempted.
586
+     * @param string An optional authorization identifier.  If specified, this
587
+     *               identifier will be used as the authorization proxy.
588
+     *
589
+     * @return mixed Returns a PEAR_Error with an error message on any
590
+     *               kind of failure, or true on success.
591
+     * @access public
592
+     * @since  1.0
593
+     */
594
+    function auth($uid, $pwd , $method = '', $tls = true, $authz = '')
595
+    {
596
+        /* We can only attempt a TLS connection if one has been requested,
597
+         * we're running PHP 5.1.0 or later, have access to the OpenSSL 
598
+         * extension, are connected to an SMTP server which supports the 
599
+         * STARTTLS extension, and aren't already connected over a secure 
600
+         * (SSL) socket connection. */
601
+        if ($tls && version_compare(PHP_VERSION, '5.1.0', '>=') &&
602
+            extension_loaded('openssl') && isset($this->_esmtp['STARTTLS']) &&
603
+            strncasecmp($this->host, 'ssl://', 6) !== 0) {
604
+            /* Start the TLS connection attempt. */
605
+            if (PEAR::isError($result = $this->_put('STARTTLS'))) {
606
+                return $result;
607
+            }
608
+            if (PEAR::isError($result = $this->_parseResponse(220))) {
609
+                return $result;
610
+            }
611
+            if (PEAR::isError($result = $this->_socket->enableCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT))) {
612
+                return $result;
613
+            } elseif ($result !== true) {
614
+                return PEAR::raiseError('STARTTLS failed');
615
+            }
616
+
617
+            /* Send EHLO again to recieve the AUTH string from the
618
+             * SMTP server. */
619
+            $this->_negotiate();
620
+        }
621
+
622
+        if (empty($this->_esmtp['AUTH'])) {
623
+            return PEAR::raiseError('SMTP server does not support authentication');
624
+        }
625
+
626
+        /* If no method has been specified, get the name of the best
627
+         * supported method advertised by the SMTP server. */
628
+        if (empty($method)) {
629
+            if (PEAR::isError($method = $this->_getBestAuthMethod())) {
630
+                /* Return the PEAR_Error object from _getBestAuthMethod(). */
631
+                return $method;
632
+            }
633
+        } else {
634
+            $method = strtoupper($method);
635
+            if (!array_key_exists($method, $this->auth_methods)) {
636
+                return PEAR::raiseError("$method is not a supported authentication method");
637
+            }
638
+        }
639
+
640
+        if (!isset($this->auth_methods[$method])) {
641
+            return PEAR::raiseError("$method is not a supported authentication method");
642
+        }
643
+
644
+        if (!is_callable($this->auth_methods[$method], false)) {
645
+            return PEAR::raiseError("$method authentication method cannot be called");
646
+        }
647
+
648
+        if (is_array($this->auth_methods[$method])) {
649
+            list($object, $method) = $this->auth_methods[$method];
650
+            $result = $object->{$method}($uid, $pwd, $authz, $this);
651
+        } else {
652
+            $func =  $this->auth_methods[$method];
653
+            $result = $func($uid, $pwd, $authz, $this);
654
+         }
655
+
656
+        /* If an error was encountered, return the PEAR_Error object. */
657
+        if (PEAR::isError($result)) {
658
+            return $result;
659
+        }
660
+
661
+        return true;
662
+    }
663
+
664
+    /**
665
+     * Add a new authentication method.
666
+     *
667
+     * @param string    The authentication method name (e.g. 'PLAIN')
668
+     * @param mixed     The authentication callback (given as the name of a 
669
+     *                  function or as an (object, method name) array).
670
+     * @param bool      Should the new method be prepended to the list of 
671
+     *                  available methods?  This is the default behavior, 
672
+     *                  giving the new method the highest priority.
673
+     *
674
+     * @return  mixed   True on success or a PEAR_Error object on failure.
675
+     *
676
+     * @access public
677
+     * @since  1.6.0
678
+     */
679
+    function setAuthMethod($name, $callback, $prepend = true)
680
+    {
681
+        if (!is_string($name)) {
682
+            return PEAR::raiseError('Method name is not a string');
683
+        }
684
+
685
+        if (!is_string($callback) && !is_array($callback)) {
686
+            return PEAR::raiseError('Method callback must be string or array');
687
+        }
688
+
689
+        if (is_array($callback)) {
690
+            if (!is_object($callback[0]) || !is_string($callback[1]))
691
+                return PEAR::raiseError('Bad mMethod callback array');
692
+        }
693
+
694
+        if ($prepend) {
695
+            $this->auth_methods = array_merge(array($name => $callback),
696
+                                              $this->auth_methods);
697
+        } else {
698
+            $this->auth_methods[$name] = $callback;
699
+        }
700
+
701
+        return true;
702
+    }
703
+
704
+    /**
705
+     * Authenticates the user using the DIGEST-MD5 method.
706
+     *
707
+     * @param string The userid to authenticate as.
708
+     * @param string The password to authenticate with.
709
+     * @param string The optional authorization proxy identifier.
710
+     *
711
+     * @return mixed Returns a PEAR_Error with an error message on any
712
+     *               kind of failure, or true on success.
713
+     * @access private
714
+     * @since  1.1.0
715
+     */
716
+    function _authDigest_MD5($uid, $pwd, $authz = '')
717
+    {
718
+        if (PEAR::isError($error = $this->_put('AUTH', 'DIGEST-MD5'))) {
719
+            return $error;
720
+        }
721
+        /* 334: Continue authentication request */
722
+        if (PEAR::isError($error = $this->_parseResponse(334))) {
723
+            /* 503: Error: already authenticated */
724
+            if ($this->_code === 503) {
725
+                return true;
726
+            }
727
+            return $error;
728
+        }
729
+
730
+        $challenge = base64_decode($this->_arguments[0]);
731
+        $digest = &Auth_SASL::factory('digestmd5');
732
+        $auth_str = base64_encode($digest->getResponse($uid, $pwd, $challenge,
733
+                                                       $this->host, "smtp",
734
+                                                       $authz));
735
+
736
+        if (PEAR::isError($error = $this->_put($auth_str))) {
737
+            return $error;
738
+        }
739
+        /* 334: Continue authentication request */
740
+        if (PEAR::isError($error = $this->_parseResponse(334))) {
741
+            return $error;
742
+        }
743
+
744
+        /* We don't use the protocol's third step because SMTP doesn't
745
+         * allow subsequent authentication, so we just silently ignore
746
+         * it. */
747
+        if (PEAR::isError($error = $this->_put(''))) {
748
+            return $error;
749
+        }
750
+        /* 235: Authentication successful */
751
+        if (PEAR::isError($error = $this->_parseResponse(235))) {
752
+            return $error;
753
+        }
754
+    }
755
+
756
+    /**
757
+     * Authenticates the user using the CRAM-MD5 method.
758
+     *
759
+     * @param string The userid to authenticate as.
760
+     * @param string The password to authenticate with.
761
+     * @param string The optional authorization proxy identifier.
762
+     *
763
+     * @return mixed Returns a PEAR_Error with an error message on any
764
+     *               kind of failure, or true on success.
765
+     * @access private
766
+     * @since  1.1.0
767
+     */
768
+    function _authCRAM_MD5($uid, $pwd, $authz = '')
769
+    {
770
+        if (PEAR::isError($error = $this->_put('AUTH', 'CRAM-MD5'))) {
771
+            return $error;
772
+        }
773
+        /* 334: Continue authentication request */
774
+        if (PEAR::isError($error = $this->_parseResponse(334))) {
775
+            /* 503: Error: already authenticated */
776
+            if ($this->_code === 503) {
777
+                return true;
778
+            }
779
+            return $error;
780
+        }
781
+
782
+        $challenge = base64_decode($this->_arguments[0]);
783
+        $cram = &Auth_SASL::factory('crammd5');
784
+        $auth_str = base64_encode($cram->getResponse($uid, $pwd, $challenge));
785
+
786
+        if (PEAR::isError($error = $this->_put($auth_str))) {
787
+            return $error;
788
+        }
789
+
790
+        /* 235: Authentication successful */
791
+        if (PEAR::isError($error = $this->_parseResponse(235))) {
792
+            return $error;
793
+        }
794
+    }
795
+
796
+    /**
797
+     * Authenticates the user using the LOGIN method.
798
+     *
799
+     * @param string The userid to authenticate as.
800
+     * @param string The password to authenticate with.
801
+     * @param string The optional authorization proxy identifier.
802
+     *
803
+     * @return mixed Returns a PEAR_Error with an error message on any
804
+     *               kind of failure, or true on success.
805
+     * @access private
806
+     * @since  1.1.0
807
+     */
808
+    function _authLogin($uid, $pwd, $authz = '')
809
+    {
810
+        if (PEAR::isError($error = $this->_put('AUTH', 'LOGIN'))) {
811
+            return $error;
812
+        }
813
+        /* 334: Continue authentication request */
814
+        if (PEAR::isError($error = $this->_parseResponse(334))) {
815
+            /* 503: Error: already authenticated */
816
+            if ($this->_code === 503) {
817
+                return true;
818
+            }
819
+            return $error;
820
+        }
821
+
822
+        if (PEAR::isError($error = $this->_put(base64_encode($uid)))) {
823
+            return $error;
824
+        }
825
+        /* 334: Continue authentication request */
826
+        if (PEAR::isError($error = $this->_parseResponse(334))) {
827
+            return $error;
828
+        }
829
+
830
+        if (PEAR::isError($error = $this->_put(base64_encode($pwd)))) {
831
+            return $error;
832
+        }
833
+
834
+        /* 235: Authentication successful */
835
+        if (PEAR::isError($error = $this->_parseResponse(235))) {
836
+            return $error;
837
+        }
838
+
839
+        return true;
840
+    }
841
+
842
+    /**
843
+     * Authenticates the user using the PLAIN method.
844
+     *
845
+     * @param string The userid to authenticate as.
846
+     * @param string The password to authenticate with.
847
+     * @param string The optional authorization proxy identifier.
848
+     *
849
+     * @return mixed Returns a PEAR_Error with an error message on any
850
+     *               kind of failure, or true on success.
851
+     * @access private
852
+     * @since  1.1.0
853
+     */
854
+    function _authPlain($uid, $pwd, $authz = '')
855
+    {
856
+        if (PEAR::isError($error = $this->_put('AUTH', 'PLAIN'))) {
857
+            return $error;
858
+        }
859
+        /* 334: Continue authentication request */
860
+        if (PEAR::isError($error = $this->_parseResponse(334))) {
861
+            /* 503: Error: already authenticated */
862
+            if ($this->_code === 503) {
863
+                return true;
864
+            }
865
+            return $error;
866
+        }
867
+
868
+        $auth_str = base64_encode($authz . chr(0) . $uid . chr(0) . $pwd);
869
+
870
+        if (PEAR::isError($error = $this->_put($auth_str))) {
871
+            return $error;
872
+        }
873
+
874
+        /* 235: Authentication successful */
875
+        if (PEAR::isError($error = $this->_parseResponse(235))) {
876
+            return $error;
877
+        }
878
+
879
+        return true;
880
+    }
881
+
882
+    /**
883
+     * Send the HELO command.
884
+     *
885
+     * @param string The domain name to say we are.
886
+     *
887
+     * @return mixed Returns a PEAR_Error with an error message on any
888
+     *               kind of failure, or true on success.
889
+     * @access public
890
+     * @since  1.0
891
+     */
892
+    function helo($domain)
893
+    {
894
+        if (PEAR::isError($error = $this->_put('HELO', $domain))) {
895
+            return $error;
896
+        }
897
+        if (PEAR::isError($error = $this->_parseResponse(250))) {
898
+            return $error;
899
+        }
900
+
901
+        return true;
902
+    }
903
+
904
+    /**
905
+     * Return the list of SMTP service extensions advertised by the server.
906
+     *
907
+     * @return array The list of SMTP service extensions.
908
+     * @access public
909
+     * @since 1.3
910
+     */
911
+    function getServiceExtensions()
912
+    {
913
+        return $this->_esmtp;
914
+    }
915
+
916
+    /**
917
+     * Send the MAIL FROM: command.
918
+     *
919
+     * @param string $sender    The sender (reverse path) to set.
920
+     * @param string $params    String containing additional MAIL parameters,
921
+     *                          such as the NOTIFY flags defined by RFC 1891
922
+     *                          or the VERP protocol.
923
+     *
924
+     *                          If $params is an array, only the 'verp' option
925
+     *                          is supported.  If 'verp' is true, the XVERP
926
+     *                          parameter is appended to the MAIL command.  If
927
+     *                          the 'verp' value is a string, the full
928
+     *                          XVERP=value parameter is appended.
929
+     *
930
+     * @return mixed Returns a PEAR_Error with an error message on any
931
+     *               kind of failure, or true on success.
932
+     * @access public
933
+     * @since  1.0
934
+     */
935
+    function mailFrom($sender, $params = null)
936
+    {
937
+        $args = "FROM:<$sender>";
938
+
939
+        /* Support the deprecated array form of $params. */
940
+        if (is_array($params) && isset($params['verp'])) {
941
+            /* XVERP */
942
+            if ($params['verp'] === true) {
943
+                $args .= ' XVERP';
944
+
945
+            /* XVERP=something */
946
+            } elseif (trim($params['verp'])) {
947
+                $args .= ' XVERP=' . $params['verp'];
948
+            }
949
+        } elseif (is_string($params) && !empty($params)) {
950
+            $args .= ' ' . $params;
951
+        }
952
+
953
+        if (PEAR::isError($error = $this->_put('MAIL', $args))) {
954
+            return $error;
955
+        }
956
+        if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) {
957
+            return $error;
958
+        }
959
+
960
+        return true;
961
+    }
962
+
963
+    /**
964
+     * Send the RCPT TO: command.
965
+     *
966
+     * @param string $recipient The recipient (forward path) to add.
967
+     * @param string $params    String containing additional RCPT parameters,
968
+     *                          such as the NOTIFY flags defined by RFC 1891.
969
+     *
970
+     * @return mixed Returns a PEAR_Error with an error message on any
971
+     *               kind of failure, or true on success.
972
+     *
973
+     * @access public
974
+     * @since  1.0
975
+     */
976
+    function rcptTo($recipient, $params = null)
977
+    {
978
+        $args = "TO:<$recipient>";
979
+        if (is_string($params)) {
980
+            $args .= ' ' . $params;
981
+        }
982
+
983
+        if (PEAR::isError($error = $this->_put('RCPT', $args))) {
984
+            return $error;
985
+        }
986
+        if (PEAR::isError($error = $this->_parseResponse(array(250, 251), $this->pipelining))) {
987
+            return $error;
988
+        }
989
+
990
+        return true;
991
+    }
992
+
993
+    /**
994
+     * Quote the data so that it meets SMTP standards.
995
+     *
996
+     * This is provided as a separate public function to facilitate
997
+     * easier overloading for the cases where it is desirable to
998
+     * customize the quoting behavior.
999
+     *
1000
+     * @param string $data  The message text to quote. The string must be passed
1001
+     *                      by reference, and the text will be modified in place.
1002
+     *
1003
+     * @access public
1004
+     * @since  1.2
1005
+     */
1006
+    function quotedata(&$data)
1007
+    {
1008
+        /* Change Unix (\n) and Mac (\r) linefeeds into
1009
+         * Internet-standard CRLF (\r\n) linefeeds. */
1010
+        $data = preg_replace(array('/(?<!\r)\n/','/\r(?!\n)/'), "\r\n", $data);
1011
+
1012
+        /* Because a single leading period (.) signifies an end to the
1013
+         * data, legitimate leading periods need to be "doubled"
1014
+         * (e.g. '..'). */
1015
+        $data = str_replace("\n.", "\n..", $data);
1016
+    }
1017
+
1018
+    /**
1019
+     * Send the DATA command.
1020
+     *
1021
+     * @param mixed $data     The message data, either as a string or an open
1022
+     *                        file resource.
1023
+     * @param string $headers The message headers.  If $headers is provided,
1024
+     *                        $data is assumed to contain only body data.
1025
+     *
1026
+     * @return mixed Returns a PEAR_Error with an error message on any
1027
+     *               kind of failure, or true on success.
1028
+     * @access public
1029
+     * @since  1.0
1030
+     */
1031
+    function data($data, $headers = null)
1032
+    {
1033
+        /* Verify that $data is a supported type. */
1034
+        if (!is_string($data) && !is_resource($data)) {
1035
+            return PEAR::raiseError('Expected a string or file resource');
1036
+        }
1037
+
1038
+        /* Start by considering the size of the optional headers string.  We
1039
+         * also account for the addition 4 character "\r\n\r\n" separator
1040
+         * sequence. */
1041
+        $size = (is_null($headers)) ? 0 : strlen($headers) + 4;
1042
+
1043
+        if (is_resource($data)) {
1044
+            $stat = fstat($data);
1045
+            if ($stat === false) {
1046
+                return PEAR::raiseError('Failed to get file size');
1047
+            }
1048
+            $size += $stat['size'];
1049
+        } else {
1050
+            $size += strlen($data);
1051
+        }
1052
+
1053
+        /* RFC 1870, section 3, subsection 3 states "a value of zero indicates
1054
+         * that no fixed maximum message size is in force".  Furthermore, it
1055
+         * says that if "the parameter is omitted no information is conveyed
1056
+         * about the server's fixed maximum message size". */
1057
+        $limit = (isset($this->_esmtp['SIZE'])) ? $this->_esmtp['SIZE'] : 0;
1058
+        if ($limit > 0 && $size >= $limit) {
1059
+            $this->disconnect();
1060
+            return PEAR::raiseError('Message size exceeds server limit');
1061
+        }
1062
+
1063
+        /* Initiate the DATA command. */
1064
+        if (PEAR::isError($error = $this->_put('DATA'))) {
1065
+            return $error;
1066
+        }
1067
+        if (PEAR::isError($error = $this->_parseResponse(354))) {
1068
+            return $error;
1069
+        }
1070
+
1071
+        /* If we have a separate headers string, send it first. */
1072
+        if (!is_null($headers)) {
1073
+            $this->quotedata($headers);
1074
+            if (PEAR::isError($result = $this->_send($headers . "\r\n\r\n"))) {
1075
+                return $result;
1076
+            }
1077
+        }
1078
+
1079
+        /* Now we can send the message body data. */
1080
+        if (is_resource($data)) {
1081
+            /* Stream the contents of the file resource out over our socket 
1082
+             * connection, line by line.  Each line must be run through the 
1083
+             * quoting routine. */
1084
+            while (strlen($line = fread($data, 8192)) > 0) {
1085
+                /* If the last character is an newline, we need to grab the
1086
+                 * next character to check to see if it is a period. */
1087
+                while (!feof($data)) {
1088
+                    $char = fread($data, 1);
1089
+                    $line .= $char;
1090
+                    if ($char != "\n") {
1091
+                        break;
1092
+                    }
1093
+                }
1094
+                $this->quotedata($line);
1095
+                if (PEAR::isError($result = $this->_send($line))) {
1096
+                    return $result;
1097
+                }
1098
+            }
1099
+        } else {
1100
+            /*
1101
+             * Break up the data by sending one chunk (up to 512k) at a time.  
1102
+             * This approach reduces our peak memory usage.
1103
+             */
1104
+            for ($offset = 0; $offset < $size;) {
1105
+                $end = $offset + 512000;
1106
+
1107
+                /*
1108
+                 * Ensure we don't read beyond our data size or span multiple 
1109
+                 * lines.  quotedata() can't properly handle character data 
1110
+                 * that's split across two line break boundaries.
1111
+                 */
1112
+                if ($end >= $size) {
1113
+                    $end = $size;
1114
+                } else {
1115
+                    for (; $end < $size; $end++) {
1116
+                        if ($data[$end] != "\n") {
1117
+                            break;
1118
+                        }
1119
+                    }
1120
+                }
1121
+
1122
+                /* Extract our chunk and run it through the quoting routine. */
1123
+                $chunk = substr($data, $offset, $end - $offset);
1124
+                $this->quotedata($chunk);
1125
+
1126
+                /* If we run into a problem along the way, abort. */
1127
+                if (PEAR::isError($result = $this->_send($chunk))) {
1128
+                    return $result;
1129
+                }
1130
+
1131
+                /* Advance the offset to the end of this chunk. */
1132
+                $offset = $end;
1133
+            }
1134
+        }
1135
+
1136
+        /* Finally, send the DATA terminator sequence. */
1137
+        if (PEAR::isError($result = $this->_send("\r\n.\r\n"))) {
1138
+            return $result;
1139
+        }
1140
+
1141
+        /* Verify that the data was successfully received by the server. */
1142
+        if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) {
1143
+            return $error;
1144
+        }
1145
+
1146
+        return true;
1147
+    }
1148
+
1149
+    /**
1150
+     * Send the SEND FROM: command.
1151
+     *
1152
+     * @param string The reverse path to send.
1153
+     *
1154
+     * @return mixed Returns a PEAR_Error with an error message on any
1155
+     *               kind of failure, or true on success.
1156
+     * @access public
1157
+     * @since  1.2.6
1158
+     */
1159
+    function sendFrom($path)
1160
+    {
1161
+        if (PEAR::isError($error = $this->_put('SEND', "FROM:<$path>"))) {
1162
+            return $error;
1163
+        }
1164
+        if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) {
1165
+            return $error;
1166
+        }
1167
+
1168
+        return true;
1169
+    }
1170
+
1171
+    /**
1172
+     * Backwards-compatibility wrapper for sendFrom().
1173
+     *
1174
+     * @param string The reverse path to send.
1175
+     *
1176
+     * @return mixed Returns a PEAR_Error with an error message on any
1177
+     *               kind of failure, or true on success.
1178
+     *
1179
+     * @access      public
1180
+     * @since       1.0
1181
+     * @deprecated  1.2.6
1182
+     */
1183
+    function send_from($path)
1184
+    {
1185
+        return sendFrom($path);
1186
+    }
1187
+
1188
+    /**
1189
+     * Send the SOML FROM: command.
1190
+     *
1191
+     * @param string The reverse path to send.
1192
+     *
1193
+     * @return mixed Returns a PEAR_Error with an error message on any
1194
+     *               kind of failure, or true on success.
1195
+     * @access public
1196
+     * @since  1.2.6
1197
+     */
1198
+    function somlFrom($path)
1199
+    {
1200
+        if (PEAR::isError($error = $this->_put('SOML', "FROM:<$path>"))) {
1201
+            return $error;
1202
+        }
1203
+        if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) {
1204
+            return $error;
1205
+        }
1206
+
1207
+        return true;
1208
+    }
1209
+
1210
+    /**
1211
+     * Backwards-compatibility wrapper for somlFrom().
1212
+     *
1213
+     * @param string The reverse path to send.
1214
+     *
1215
+     * @return mixed Returns a PEAR_Error with an error message on any
1216
+     *               kind of failure, or true on success.
1217
+     *
1218
+     * @access      public
1219
+     * @since       1.0
1220
+     * @deprecated  1.2.6
1221
+     */
1222
+    function soml_from($path)
1223
+    {
1224
+        return somlFrom($path);
1225
+    }
1226
+
1227
+    /**
1228
+     * Send the SAML FROM: command.
1229
+     *
1230
+     * @param string The reverse path to send.
1231
+     *
1232
+     * @return mixed Returns a PEAR_Error with an error message on any
1233
+     *               kind of failure, or true on success.
1234
+     * @access public
1235
+     * @since  1.2.6
1236
+     */
1237
+    function samlFrom($path)
1238
+    {
1239
+        if (PEAR::isError($error = $this->_put('SAML', "FROM:<$path>"))) {
1240
+            return $error;
1241
+        }
1242
+        if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) {
1243
+            return $error;
1244
+        }
1245
+
1246
+        return true;
1247
+    }
1248
+
1249
+    /**
1250
+     * Backwards-compatibility wrapper for samlFrom().
1251
+     *
1252
+     * @param string The reverse path to send.
1253
+     *
1254
+     * @return mixed Returns a PEAR_Error with an error message on any
1255
+     *               kind of failure, or true on success.
1256
+     *
1257
+     * @access      public
1258
+     * @since       1.0
1259
+     * @deprecated  1.2.6
1260
+     */
1261
+    function saml_from($path)
1262
+    {
1263
+        return samlFrom($path);
1264
+    }
1265
+
1266
+    /**
1267
+     * Send the RSET command.
1268
+     *
1269
+     * @return mixed Returns a PEAR_Error with an error message on any
1270
+     *               kind of failure, or true on success.
1271
+     * @access public
1272
+     * @since  1.0
1273
+     */
1274
+    function rset()
1275
+    {
1276
+        if (PEAR::isError($error = $this->_put('RSET'))) {
1277
+            return $error;
1278
+        }
1279
+        if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) {
1280
+            return $error;
1281
+        }
1282
+
1283
+        return true;
1284
+    }
1285
+
1286
+    /**
1287
+     * Send the VRFY command.
1288
+     *
1289
+     * @param string The string to verify
1290
+     *
1291
+     * @return mixed Returns a PEAR_Error with an error message on any
1292
+     *               kind of failure, or true on success.
1293
+     * @access public
1294
+     * @since  1.0
1295
+     */
1296
+    function vrfy($string)
1297
+    {
1298
+        /* Note: 251 is also a valid response code */
1299
+        if (PEAR::isError($error = $this->_put('VRFY', $string))) {
1300
+            return $error;
1301
+        }
1302
+        if (PEAR::isError($error = $this->_parseResponse(array(250, 252)))) {
1303
+            return $error;
1304
+        }
1305
+
1306
+        return true;
1307
+    }
1308
+
1309
+    /**
1310
+     * Send the NOOP command.
1311
+     *
1312
+     * @return mixed Returns a PEAR_Error with an error message on any
1313
+     *               kind of failure, or true on success.
1314
+     * @access public
1315
+     * @since  1.0
1316
+     */
1317
+    function noop()
1318
+    {
1319
+        if (PEAR::isError($error = $this->_put('NOOP'))) {
1320
+            return $error;
1321
+        }
1322
+        if (PEAR::isError($error = $this->_parseResponse(250))) {
1323
+            return $error;
1324
+        }
1325
+
1326
+        return true;
1327
+    }
1328
+
1329
+    /**
1330
+     * Backwards-compatibility method.  identifySender()'s functionality is
1331
+     * now handled internally.
1332
+     *
1333
+     * @return  boolean     This method always return true.
1334
+     *
1335
+     * @access  public
1336
+     * @since   1.0
1337
+     */
1338
+    function identifySender()
1339
+    {
1340
+        return true;
1341
+    }
1342
+
1343
+}
1344
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Net/Socket.php Added
655
 
1
@@ -0,0 +1,653 @@
2
+<?php
3
+/**
4
+ * Net_Socket
5
+ *
6
+ * PHP Version 4
7
+ *
8
+ * Copyright (c) 1997-2003 The PHP Group
9
+ *
10
+ * This source file is subject to version 2.0 of the PHP license,
11
+ * that is bundled with this package in the file LICENSE, and is
12
+ * available at through the world-wide-web at
13
+ * http://www.php.net/license/2_02.txt.
14
+ * If you did not receive a copy of the PHP license and are unable to
15
+ * obtain it through the world-wide-web, please send a note to
16
+ * license@php.net so we can mail you a copy immediately.
17
+ *
18
+ * Authors: Stig Bakken <ssb@php.net>
19
+ *          Chuck Hagenbuch <chuck@horde.org>
20
+ *
21
+ * @category  Net
22
+ * @package   Net_Socket
23
+ * @author    Stig Bakken <ssb@php.net>
24
+ * @author    Chuck Hagenbuch <chuck@horde.org>
25
+ * @copyright 1997-2003 The PHP Group
26
+ * @license   http://www.php.net/license/2_02.txt PHP 2.02
27
+ * @version   CVS: $Id$
28
+ * @link      http://pear.php.net/packages/Net_Socket
29
+ */
30
+
31
+require_once 'PEAR.php';
32
+
33
+define('NET_SOCKET_READ', 1);
34
+define('NET_SOCKET_WRITE', 2);
35
+define('NET_SOCKET_ERROR', 4);
36
+
37
+/**
38
+ * Generalized Socket class.
39
+ *
40
+ * @category  Net
41
+ * @package   Net_Socket
42
+ * @author    Stig Bakken <ssb@php.net>
43
+ * @author    Chuck Hagenbuch <chuck@horde.org>
44
+ * @copyright 1997-2003 The PHP Group
45
+ * @license   http://www.php.net/license/2_02.txt PHP 2.02
46
+ * @link      http://pear.php.net/packages/Net_Socket
47
+ */
48
+class Net_Socket extends PEAR
49
+{
50
+    /**
51
+     * Socket file pointer.
52
+     * @var resource $fp
53
+     */
54
+    var $fp = null;
55
+
56
+    /**
57
+     * Whether the socket is blocking. Defaults to true.
58
+     * @var boolean $blocking
59
+     */
60
+    var $blocking = true;
61
+
62
+    /**
63
+     * Whether the socket is persistent. Defaults to false.
64
+     * @var boolean $persistent
65
+     */
66
+    var $persistent = false;
67
+
68
+    /**
69
+     * The IP address to connect to.
70
+     * @var string $addr
71
+     */
72
+    var $addr = '';
73
+
74
+    /**
75
+     * The port number to connect to.
76
+     * @var integer $port
77
+     */
78
+    var $port = 0;
79
+
80
+    /**
81
+     * Number of seconds to wait on socket connections before assuming
82
+     * there's no more data. Defaults to no timeout.
83
+     * @var integer $timeout
84
+     */
85
+    var $timeout = false;
86
+
87
+    /**
88
+     * Number of bytes to read at a time in readLine() and
89
+     * readAll(). Defaults to 2048.
90
+     * @var integer $lineLength
91
+     */
92
+    var $lineLength = 2048;
93
+
94
+    /**
95
+     * The string to use as a newline terminator. Usually "\r\n" or "\n".
96
+     * @var string $newline
97
+     */
98
+    var $newline = "\r\n";
99
+
100
+    /**
101
+     * Connect to the specified port. If called when the socket is
102
+     * already connected, it disconnects and connects again.
103
+     *
104
+     * @param string  $addr       IP address or host name.
105
+     * @param integer $port       TCP port number.
106
+     * @param boolean $persistent (optional) Whether the connection is
107
+     *                            persistent (kept open between requests
108
+     *                            by the web server).
109
+     * @param integer $timeout    (optional) How long to wait for data.
110
+     * @param array   $options    See options for stream_context_create.
111
+     *
112
+     * @access public
113
+     *
114
+     * @return boolean | PEAR_Error  True on success or a PEAR_Error on failure.
115
+     */
116
+    function connect($addr, $port = 0, $persistent = null,
117
+                     $timeout = null, $options = null)
118
+    {
119
+        if (is_resource($this->fp)) {
120
+            @fclose($this->fp);
121
+            $this->fp = null;
122
+        }
123
+
124
+        if (!$addr) {
125
+            return $this->raiseError('$addr cannot be empty');
126
+        } elseif (strspn($addr, '.0123456789') == strlen($addr) ||
127
+                  strstr($addr, '/') !== false) {
128
+            $this->addr = $addr;
129
+        } else {
130
+            $this->addr = @gethostbyname($addr);
131
+        }
132
+
133
+        $this->port = $port % 65536;
134
+
135
+        if ($persistent !== null) {
136
+            $this->persistent = $persistent;
137
+        }
138
+
139
+        if ($timeout !== null) {
140
+            $this->timeout = $timeout;
141
+        }
142
+
143
+        $openfunc = $this->persistent ? 'pfsockopen' : 'fsockopen';
144
+        $errno    = 0;
145
+        $errstr   = '';
146
+
147
+        $old_track_errors = @ini_set('track_errors', 1);
148
+
149
+        if ($options && function_exists('stream_context_create')) {
150
+            if ($this->timeout) {
151
+                $timeout = $this->timeout;
152
+            } else {
153
+                $timeout = 0;
154
+            }
155
+            $context = stream_context_create($options);
156
+
157
+            // Since PHP 5 fsockopen doesn't allow context specification
158
+            if (function_exists('stream_socket_client')) {
159
+                $flags = STREAM_CLIENT_CONNECT;
160
+
161
+                if ($this->persistent) {
162
+                    $flags = STREAM_CLIENT_PERSISTENT;
163
+                }
164
+
165
+                $addr = $this->addr . ':' . $this->port;
166
+                $fp   = stream_socket_client($addr, $errno, $errstr,
167
+                                             $timeout, $flags, $context);
168
+            } else {
169
+                $fp = @$openfunc($this->addr, $this->port, $errno,
170
+                                 $errstr, $timeout, $context);
171
+            }
172
+        } else {
173
+            if ($this->timeout) {
174
+                $fp = @$openfunc($this->addr, $this->port, $errno,
175
+                                 $errstr, $this->timeout);
176
+            } else {
177
+                $fp = @$openfunc($this->addr, $this->port, $errno, $errstr);
178
+            }
179
+        }
180
+
181
+        if (!$fp) {
182
+            if ($errno == 0 && !strlen($errstr) && isset($php_errormsg)) {
183
+                $errstr = $php_errormsg;
184
+            }
185
+            @ini_set('track_errors', $old_track_errors);
186
+            return $this->raiseError($errstr, $errno);
187
+        }
188
+
189
+        @ini_set('track_errors', $old_track_errors);
190
+        $this->fp = $fp;
191
+
192
+        return $this->setBlocking($this->blocking);
193
+    }
194
+
195
+    /**
196
+     * Disconnects from the peer, closes the socket.
197
+     *
198
+     * @access public
199
+     * @return mixed true on success or a PEAR_Error instance otherwise
200
+     */
201
+    function disconnect()
202
+    {
203
+        if (!is_resource($this->fp)) {
204
+            return $this->raiseError('not connected');
205
+        }
206
+
207
+        @fclose($this->fp);
208
+        $this->fp = null;
209
+        return true;
210
+    }
211
+
212
+    /**
213
+     * Set the newline character/sequence to use.
214
+     *
215
+     * @param string $newline  Newline character(s)
216
+     * @return boolean True
217
+     */
218
+    function setNewline($newline)
219
+    {
220
+        $this->newline = $newline;
221
+        return true;
222
+    }
223
+
224
+    /**
225
+     * Find out if the socket is in blocking mode.
226
+     *
227
+     * @access public
228
+     * @return boolean  The current blocking mode.
229
+     */
230
+    function isBlocking()
231
+    {
232
+        return $this->blocking;
233
+    }
234
+
235
+    /**
236
+     * Sets whether the socket connection should be blocking or
237
+     * not. A read call to a non-blocking socket will return immediately
238
+     * if there is no data available, whereas it will block until there
239
+     * is data for blocking sockets.
240
+     *
241
+     * @param boolean $mode True for blocking sockets, false for nonblocking.
242
+     *
243
+     * @access public
244
+     * @return mixed true on success or a PEAR_Error instance otherwise
245
+     */
246
+    function setBlocking($mode)
247
+    {
248
+        if (!is_resource($this->fp)) {
249
+            return $this->raiseError('not connected');
250
+        }
251
+
252
+        $this->blocking = $mode;
253
+        stream_set_blocking($this->fp, (int)$this->blocking);
254
+        return true;
255
+    }
256
+
257
+    /**
258
+     * Sets the timeout value on socket descriptor,
259
+     * expressed in the sum of seconds and microseconds
260
+     *
261
+     * @param integer $seconds      Seconds.
262
+     * @param integer $microseconds Microseconds.
263
+     *
264
+     * @access public
265
+     * @return mixed true on success or a PEAR_Error instance otherwise
266
+     */
267
+    function setTimeout($seconds, $microseconds)
268
+    {
269
+        if (!is_resource($this->fp)) {
270
+            return $this->raiseError('not connected');
271
+        }
272
+
273
+        return socket_set_timeout($this->fp, $seconds, $microseconds);
274
+    }
275
+
276
+    /**
277
+     * Sets the file buffering size on the stream.
278
+     * See php's stream_set_write_buffer for more information.
279
+     *
280
+     * @param integer $size Write buffer size.
281
+     *
282
+     * @access public
283
+     * @return mixed on success or an PEAR_Error object otherwise
284
+     */
285
+    function setWriteBuffer($size)
286
+    {
287
+        if (!is_resource($this->fp)) {
288
+            return $this->raiseError('not connected');
289
+        }
290
+
291
+        $returned = stream_set_write_buffer($this->fp, $size);
292
+        if ($returned == 0) {
293
+            return true;
294
+        }
295
+        return $this->raiseError('Cannot set write buffer.');
296
+    }
297
+
298
+    /**
299
+     * Returns information about an existing socket resource.
300
+     * Currently returns four entries in the result array:
301
+     *
302
+     * <p>
303
+     * timed_out (bool) - The socket timed out waiting for data<br>
304
+     * blocked (bool) - The socket was blocked<br>
305
+     * eof (bool) - Indicates EOF event<br>
306
+     * unread_bytes (int) - Number of bytes left in the socket buffer<br>
307
+     * </p>
308
+     *
309
+     * @access public
310
+     * @return mixed Array containing information about existing socket
311
+     *               resource or a PEAR_Error instance otherwise
312
+     */
313
+    function getStatus()
314
+    {
315
+        if (!is_resource($this->fp)) {
316
+            return $this->raiseError('not connected');
317
+        }
318
+
319
+        return socket_get_status($this->fp);
320
+    }
321
+
322
+    /**
323
+     * Get a specified line of data
324
+     *
325
+     * @param int $size ??
326
+     *
327
+     * @access public
328
+     * @return $size bytes of data from the socket, or a PEAR_Error if
329
+     *         not connected.
330
+     */
331
+    function gets($size = null)
332
+    {
333
+        if (!is_resource($this->fp)) {
334
+            return $this->raiseError('not connected');
335
+        }
336
+
337
+        if (is_null($size)) {
338
+            return @fgets($this->fp);
339
+        } else {
340
+            return @fgets($this->fp, $size);
341
+        }
342
+    }
343
+
344
+    /**
345
+     * Read a specified amount of data. This is guaranteed to return,
346
+     * and has the added benefit of getting everything in one fread()
347
+     * chunk; if you know the size of the data you're getting
348
+     * beforehand, this is definitely the way to go.
349
+     *
350
+     * @param integer $size The number of bytes to read from the socket.
351
+     *
352
+     * @access public
353
+     * @return $size bytes of data from the socket, or a PEAR_Error if
354
+     *         not connected.
355
+     */
356
+    function read($size)
357
+    {
358
+        if (!is_resource($this->fp)) {
359
+            return $this->raiseError('not connected');
360
+        }
361
+
362
+        return @fread($this->fp, $size);
363
+    }
364
+
365
+    /**
366
+     * Write a specified amount of data.
367
+     *
368
+     * @param string  $data      Data to write.
369
+     * @param integer $blocksize Amount of data to write at once.
370
+     *                           NULL means all at once.
371
+     *
372
+     * @access public
373
+     * @return mixed If the socket is not connected, returns an instance of
374
+     *               PEAR_Error
375
+     *               If the write succeeds, returns the number of bytes written
376
+     *               If the write fails, returns false.
377
+     */
378
+    function write($data, $blocksize = null)
379
+    {
380
+        if (!is_resource($this->fp)) {
381
+            return $this->raiseError('not connected');
382
+        }
383
+
384
+        if (is_null($blocksize) && !OS_WINDOWS) {
385
+            return @fwrite($this->fp, $data);
386
+        } else {
387
+            if (is_null($blocksize)) {
388
+                $blocksize = 1024;
389
+            }
390
+
391
+            $pos  = 0;
392
+            $size = strlen($data);
393
+            while ($pos < $size) {
394
+                $written = @fwrite($this->fp, substr($data, $pos, $blocksize));
395
+                if (!$written) {
396
+                    return $written;
397
+                }
398
+                $pos += $written;
399
+            }
400
+
401
+            return $pos;
402
+        }
403
+    }
404
+
405
+    /**
406
+     * Write a line of data to the socket, followed by a trailing newline.
407
+     *
408
+     * @param string $data Data to write
409
+     *
410
+     * @access public
411
+     * @return mixed fputs result, or an error
412
+     */
413
+    function writeLine($data)
414
+    {
415
+        if (!is_resource($this->fp)) {
416
+            return $this->raiseError('not connected');
417
+        }
418
+
419
+        return fwrite($this->fp, $data . $this->newline);
420
+    }
421
+
422
+    /**
423
+     * Tests for end-of-file on a socket descriptor.
424
+     *
425
+     * Also returns true if the socket is disconnected.
426
+     *
427
+     * @access public
428
+     * @return bool
429
+     */
430
+    function eof()
431
+    {
432
+        return (!is_resource($this->fp) || feof($this->fp));
433
+    }
434
+
435
+    /**
436
+     * Reads a byte of data
437
+     *
438
+     * @access public
439
+     * @return 1 byte of data from the socket, or a PEAR_Error if
440
+     *         not connected.
441
+     */
442
+    function readByte()
443
+    {
444
+        if (!is_resource($this->fp)) {
445
+            return $this->raiseError('not connected');
446
+        }
447
+
448
+        return ord(@fread($this->fp, 1));
449
+    }
450
+
451
+    /**
452
+     * Reads a word of data
453
+     *
454
+     * @access public
455
+     * @return 1 word of data from the socket, or a PEAR_Error if
456
+     *         not connected.
457
+     */
458
+    function readWord()
459
+    {
460
+        if (!is_resource($this->fp)) {
461
+            return $this->raiseError('not connected');
462
+        }
463
+
464
+        $buf = @fread($this->fp, 2);
465
+        return (ord($buf[0]) + (ord($buf[1]) << 8));
466
+    }
467
+
468
+    /**
469
+     * Reads an int of data
470
+     *
471
+     * @access public
472
+     * @return integer  1 int of data from the socket, or a PEAR_Error if
473
+     *                  not connected.
474
+     */
475
+    function readInt()
476
+    {
477
+        if (!is_resource($this->fp)) {
478
+            return $this->raiseError('not connected');
479
+        }
480
+
481
+        $buf = @fread($this->fp, 4);
482
+        return (ord($buf[0]) + (ord($buf[1]) << 8) +
483
+                (ord($buf[2]) << 16) + (ord($buf[3]) << 24));
484
+    }
485
+
486
+    /**
487
+     * Reads a zero-terminated string of data
488
+     *
489
+     * @access public
490
+     * @return string, or a PEAR_Error if
491
+     *         not connected.
492
+     */
493
+    function readString()
494
+    {
495
+        if (!is_resource($this->fp)) {
496
+            return $this->raiseError('not connected');
497
+        }
498
+
499
+        $string = '';
500
+        while (($char = @fread($this->fp, 1)) != "\x00") {
501
+            $string .= $char;
502
+        }
503
+        return $string;
504
+    }
505
+
506
+    /**
507
+     * Reads an IP Address and returns it in a dot formatted string
508
+     *
509
+     * @access public
510
+     * @return Dot formatted string, or a PEAR_Error if
511
+     *         not connected.
512
+     */
513
+    function readIPAddress()
514
+    {
515
+        if (!is_resource($this->fp)) {
516
+            return $this->raiseError('not connected');
517
+        }
518
+
519
+        $buf = @fread($this->fp, 4);
520
+        return sprintf('%d.%d.%d.%d', ord($buf[0]), ord($buf[1]),
521
+                       ord($buf[2]), ord($buf[3]));
522
+    }
523
+
524
+    /**
525
+     * Read until either the end of the socket or a newline, whichever
526
+     * comes first. Strips the trailing newline from the returned data.
527
+     *
528
+     * @access public
529
+     * @return All available data up to a newline, without that
530
+     *         newline, or until the end of the socket, or a PEAR_Error if
531
+     *         not connected.
532
+     */
533
+    function readLine()
534
+    {
535
+        if (!is_resource($this->fp)) {
536
+            return $this->raiseError('not connected');
537
+        }
538
+
539
+        $line = '';
540
+
541
+        $timeout = time() + $this->timeout;
542
+
543
+        while (!feof($this->fp) && (!$this->timeout || time() < $timeout)) {
544
+            $line .= @fgets($this->fp, $this->lineLength);
545
+            if (substr($line, -1) == "\n") {
546
+                return rtrim($line, $this->newline);
547
+            }
548
+        }
549
+        return $line;
550
+    }
551
+
552
+    /**
553
+     * Read until the socket closes, or until there is no more data in
554
+     * the inner PHP buffer. If the inner buffer is empty, in blocking
555
+     * mode we wait for at least 1 byte of data. Therefore, in
556
+     * blocking mode, if there is no data at all to be read, this
557
+     * function will never exit (unless the socket is closed on the
558
+     * remote end).
559
+     *
560
+     * @access public
561
+     *
562
+     * @return string  All data until the socket closes, or a PEAR_Error if
563
+     *                 not connected.
564
+     */
565
+    function readAll()
566
+    {
567
+        if (!is_resource($this->fp)) {
568
+            return $this->raiseError('not connected');
569
+        }
570
+
571
+        $data = '';
572
+        while (!feof($this->fp)) {
573
+            $data .= @fread($this->fp, $this->lineLength);
574
+        }
575
+        return $data;
576
+    }
577
+
578
+    /**
579
+     * Runs the equivalent of the select() system call on the socket
580
+     * with a timeout specified by tv_sec and tv_usec.
581
+     *
582
+     * @param integer $state   Which of read/write/error to check for.
583
+     * @param integer $tv_sec  Number of seconds for timeout.
584
+     * @param integer $tv_usec Number of microseconds for timeout.
585
+     *
586
+     * @access public
587
+     * @return False if select fails, integer describing which of read/write/error
588
+     *         are ready, or PEAR_Error if not connected.
589
+     */
590
+    function select($state, $tv_sec, $tv_usec = 0)
591
+    {
592
+        if (!is_resource($this->fp)) {
593
+            return $this->raiseError('not connected');
594
+        }
595
+
596
+        $read   = null;
597
+        $write  = null;
598
+        $except = null;
599
+        if ($state & NET_SOCKET_READ) {
600
+            $read[] = $this->fp;
601
+        }
602
+        if ($state & NET_SOCKET_WRITE) {
603
+            $write[] = $this->fp;
604
+        }
605
+        if ($state & NET_SOCKET_ERROR) {
606
+            $except[] = $this->fp;
607
+        }
608
+        if (false === ($sr = stream_select($read, $write, $except,
609
+                                          $tv_sec, $tv_usec))) {
610
+            return false;
611
+        }
612
+
613
+        $result = 0;
614
+        if (count($read)) {
615
+            $result |= NET_SOCKET_READ;
616
+        }
617
+        if (count($write)) {
618
+            $result |= NET_SOCKET_WRITE;
619
+        }
620
+        if (count($except)) {
621
+            $result |= NET_SOCKET_ERROR;
622
+        }
623
+        return $result;
624
+    }
625
+
626
+    /**
627
+     * Turns encryption on/off on a connected socket.
628
+     *
629
+     * @param bool    $enabled Set this parameter to true to enable encryption
630
+     *                         and false to disable encryption.
631
+     * @param integer $type    Type of encryption. See stream_socket_enable_crypto()
632
+     *                         for values.
633
+     *
634
+     * @see    http://se.php.net/manual/en/function.stream-socket-enable-crypto.php
635
+     * @access public
636
+     * @return false on error, true on success and 0 if there isn't enough data
637
+     *         and the user should try again (non-blocking sockets only).
638
+     *         A PEAR_Error object is returned if the socket is not
639
+     *         connected
640
+     */
641
+    function enableCrypto($enabled, $type)
642
+    {
643
+        if (version_compare(phpversion(), "5.1.0", ">=")) {
644
+            if (!is_resource($this->fp)) {
645
+                return $this->raiseError('not connected');
646
+            }
647
+            return @stream_socket_enable_crypto($this->fp, $enabled, $type);
648
+        } else {
649
+            $msg = 'Net_Socket::enableCrypto() requires php version >= 5.1.0';
650
+            return $this->raiseError($msg);
651
+        }
652
+    }
653
+
654
+}
655
iRony-0.4.4.tar.gz/lib/FileAPI/ext/Net/URL2.php Added
944
 
1
@@ -0,0 +1,942 @@
2
+<?php
3
+/**
4
+ * Net_URL2, a class representing a URL as per RFC 3986.
5
+ *
6
+ * PHP version 5
7
+ *
8
+ * LICENSE:
9
+ *
10
+ * Copyright (c) 2007-2009, Peytz & Co. A/S
11
+ * All rights reserved.
12
+ *
13
+ * Redistribution and use in source and binary forms, with or without
14
+ * modification, are permitted provided that the following conditions
15
+ * are met:
16
+ *
17
+ *   * Redistributions of source code must retain the above copyright
18
+ *     notice, this list of conditions and the following disclaimer.
19
+ *   * Redistributions in binary form must reproduce the above copyright
20
+ *     notice, this list of conditions and the following disclaimer in
21
+ *     the documentation and/or other materials provided with the distribution.
22
+ *   * Neither the name of the Net_URL2 nor the names of its contributors may
23
+ *     be used to endorse or promote products derived from this software
24
+ *     without specific prior written permission.
25
+ *
26
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
27
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
28
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
29
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
30
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
31
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
32
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
33
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
34
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
35
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
36
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37
+ *
38
+ * @category  Networking
39
+ * @package   Net_URL2
40
+ * @author    Christian Schmidt <schmidt@php.net>
41
+ * @copyright 2007-2009 Peytz & Co. A/S
42
+ * @license   http://www.opensource.org/licenses/bsd-license.php New BSD License
43
+ * @version   CVS: $Id: URL2.php 309223 2011-03-14 14:26:32Z till $
44
+ * @link      http://www.rfc-editor.org/rfc/rfc3986.txt
45
+ */
46
+
47
+/**
48
+ * Represents a URL as per RFC 3986.
49
+ *
50
+ * @category  Networking
51
+ * @package   Net_URL2
52
+ * @author    Christian Schmidt <schmidt@php.net>
53
+ * @copyright 2007-2009 Peytz & Co. A/S
54
+ * @license   http://www.opensource.org/licenses/bsd-license.php New BSD License
55
+ * @version   Release: @package_version@
56
+ * @link      http://pear.php.net/package/Net_URL2
57
+ */
58
+class Net_URL2
59
+{
60
+    /**
61
+     * Do strict parsing in resolve() (see RFC 3986, section 5.2.2). Default
62
+     * is true.
63
+     */
64
+    const OPTION_STRICT = 'strict';
65
+
66
+    /**
67
+     * Represent arrays in query using PHP's [] notation. Default is true.
68
+     */
69
+    const OPTION_USE_BRACKETS = 'use_brackets';
70
+
71
+    /**
72
+     * URL-encode query variable keys. Default is true.
73
+     */
74
+    const OPTION_ENCODE_KEYS = 'encode_keys';
75
+
76
+    /**
77
+     * Query variable separators when parsing the query string. Every character
78
+     * is considered a separator. Default is "&".
79
+     */
80
+    const OPTION_SEPARATOR_INPUT = 'input_separator';
81
+
82
+    /**
83
+     * Query variable separator used when generating the query string. Default
84
+     * is "&".
85
+     */
86
+    const OPTION_SEPARATOR_OUTPUT = 'output_separator';
87
+
88
+    /**
89
+     * Default options corresponds to how PHP handles $_GET.
90
+     */
91
+    private $_options = array(
92
+        self::OPTION_STRICT           => true,
93
+        self::OPTION_USE_BRACKETS     => true,
94
+        self::OPTION_ENCODE_KEYS      => true,
95
+        self::OPTION_SEPARATOR_INPUT  => '&',
96
+        self::OPTION_SEPARATOR_OUTPUT => '&',
97
+        );
98
+
99
+    /**
100
+     * @var  string|bool
101
+     */
102
+    private $_scheme = false;
103
+
104
+    /**
105
+     * @var  string|bool
106
+     */
107
+    private $_userinfo = false;
108
+
109
+    /**
110
+     * @var  string|bool
111
+     */
112
+    private $_host = false;
113
+
114
+    /**
115
+     * @var  string|bool
116
+     */
117
+    private $_port = false;
118
+
119
+    /**
120
+     * @var  string
121
+     */
122
+    private $_path = '';
123
+
124
+    /**
125
+     * @var  string|bool
126
+     */
127
+    private $_query = false;
128
+
129
+    /**
130
+     * @var  string|bool
131
+     */
132
+    private $_fragment = false;
133
+
134
+    /**
135
+     * Constructor.
136
+     *
137
+     * @param string $url     an absolute or relative URL
138
+     * @param array  $options an array of OPTION_xxx constants
139
+     *
140
+     * @return $this
141
+     * @uses   self::parseUrl()
142
+     */
143
+    public function __construct($url, array $options = array())
144
+    {
145
+        foreach ($options as $optionName => $value) {
146
+            if (array_key_exists($optionName, $this->_options)) {
147
+                $this->_options[$optionName] = $value;
148
+            }
149
+        }
150
+
151
+        $this->parseUrl($url);
152
+    }
153
+
154
+    /**
155
+     * Magic Setter.
156
+     *
157
+     * This method will magically set the value of a private variable ($var)
158
+     * with the value passed as the args
159
+     *
160
+     * @param  string $var      The private variable to set.
161
+     * @param  mixed  $arg      An argument of any type.
162
+     * @return void
163
+     */
164
+    public function __set($var, $arg)
165
+    {
166
+        $method = 'set' . $var;
167
+        if (method_exists($this, $method)) {
168
+            $this->$method($arg);
169
+        }
170
+    }
171
+
172
+    /**
173
+     * Magic Getter.
174
+     *
175
+     * This is the magic get method to retrieve the private variable
176
+     * that was set by either __set() or it's setter...
177
+     *
178
+     * @param  string $var         The property name to retrieve.
179
+     * @return mixed  $this->$var  Either a boolean false if the
180
+     *                             property is not set or the value
181
+     *                             of the private property.
182
+     */
183
+    public function __get($var)
184
+    {
185
+        $method = 'get' . $var;
186
+        if (method_exists($this, $method)) {
187
+            return $this->$method();
188
+        }
189
+
190
+        return false;
191
+    }
192
+
193
+    /**
194
+     * Returns the scheme, e.g. "http" or "urn", or false if there is no
195
+     * scheme specified, i.e. if this is a relative URL.
196
+     *
197
+     * @return  string|bool
198
+     */
199
+    public function getScheme()
200
+    {
201
+        return $this->_scheme;
202
+    }
203
+
204
+    /**
205
+     * Sets the scheme, e.g. "http" or "urn". Specify false if there is no
206
+     * scheme specified, i.e. if this is a relative URL.
207
+     *
208
+     * @param string|bool $scheme e.g. "http" or "urn", or false if there is no
209
+     *                            scheme specified, i.e. if this is a relative
210
+     *                            URL
211
+     *
212
+     * @return $this
213
+     * @see    getScheme()
214
+     */
215
+    public function setScheme($scheme)
216
+    {
217
+        $this->_scheme = $scheme;
218
+        return $this;
219
+    }
220
+
221
+    /**
222
+     * Returns the user part of the userinfo part (the part preceding the first
223
+     *  ":"), or false if there is no userinfo part.
224
+     *
225
+     * @return  string|bool
226
+     */
227
+    public function getUser()
228
+    {
229
+        return $this->_userinfo !== false
230
+            ? preg_replace('@:.*$@', '', $this->_userinfo)
231
+            : false;
232
+    }
233
+
234
+    /**
235
+     * Returns the password part of the userinfo part (the part after the first
236
+     *  ":"), or false if there is no userinfo part (i.e. the URL does not
237
+     * contain "@" in front of the hostname) or the userinfo part does not
238
+     * contain ":".
239
+     *
240
+     * @return  string|bool
241
+     */
242
+    public function getPassword()
243
+    {
244
+        return $this->_userinfo !== false
245
+            ? substr(strstr($this->_userinfo, ':'), 1)
246
+            : false;
247
+    }
248
+
249
+    /**
250
+     * Returns the userinfo part, or false if there is none, i.e. if the
251
+     * authority part does not contain "@".
252
+     *
253
+     * @return  string|bool
254
+     */
255
+    public function getUserinfo()
256
+    {
257
+        return $this->_userinfo;
258
+    }
259
+
260
+    /**
261
+     * Sets the userinfo part. If two arguments are passed, they are combined
262
+     * in the userinfo part as username ":" password.
263
+     *
264
+     * @param string|bool $userinfo userinfo or username
265
+     * @param string|bool $password optional password, or false
266
+     *
267
+     * @return $this
268
+     */
269
+    public function setUserinfo($userinfo, $password = false)
270
+    {
271
+        $this->_userinfo = $userinfo;
272
+        if ($password !== false) {
273
+            $this->_userinfo .= ':' . $password;
274
+        }
275
+        return $this;
276
+    }
277
+
278
+    /**
279
+     * Returns the host part, or false if there is no authority part, e.g.
280
+     * relative URLs.
281
+     *
282
+     * @return  string|bool a hostname, an IP address, or false
283
+     */
284
+    public function getHost()
285
+    {
286
+        return $this->_host;
287
+    }
288
+
289
+    /**
290
+     * Sets the host part. Specify false if there is no authority part, e.g.
291
+     * relative URLs.
292
+     *
293
+     * @param string|bool $host a hostname, an IP address, or false
294
+     *
295
+     * @return $this
296
+     */
297
+    public function setHost($host)
298
+    {
299
+        $this->_host = $host;
300
+        return $this;
301
+    }
302
+
303
+    /**
304
+     * Returns the port number, or false if there is no port number specified,
305
+     * i.e. if the default port is to be used.
306
+     *
307
+     * @return  string|bool
308
+     */
309
+    public function getPort()
310
+    {
311
+        return $this->_port;
312
+    }
313
+
314
+    /**
315
+     * Sets the port number. Specify false if there is no port number specified,
316
+     * i.e. if the default port is to be used.
317
+     *
318
+     * @param string|bool $port a port number, or false
319
+     *
320
+     * @return $this
321
+     */
322
+    public function setPort($port)
323
+    {
324
+        $this->_port = $port;
325
+        return $this;
326
+    }
327
+
328
+    /**
329
+     * Returns the authority part, i.e. [ userinfo "@" ] host [ ":" port ], or
330
+     * false if there is no authority.
331
+     *
332
+     * @return string|bool
333
+     */
334
+    public function getAuthority()
335
+    {
336
+        if (!$this->_host) {
337
+            return false;
338
+        }
339
+
340
+        $authority = '';
341
+
342
+        if ($this->_userinfo !== false) {
343
+            $authority .= $this->_userinfo . '@';
344
+        }
345
+
346
+        $authority .= $this->_host;
347
+
348
+        if ($this->_port !== false) {
349
+            $authority .= ':' . $this->_port;
350
+        }
351
+
352
+        return $authority;
353
+    }
354
+
355
+    /**
356
+     * Sets the authority part, i.e. [ userinfo "@" ] host [ ":" port ]. Specify
357
+     * false if there is no authority.
358
+     *
359
+     * @param string|false $authority a hostname or an IP addresse, possibly
360
+     *                                with userinfo prefixed and port number
361
+     *                                appended, e.g. "foo:bar@example.org:81".
362
+     *
363
+     * @return $this
364
+     */
365
+    public function setAuthority($authority)
366
+    {
367
+        $this->_userinfo = false;
368
+        $this->_host     = false;
369
+        $this->_port     = false;
370
+        if (preg_match('@^(([^\@]*)\@)?([^:]+)(:(\d*))?$@', $authority, $reg)) {
371
+            if ($reg[1]) {
372
+                $this->_userinfo = $reg[2];
373
+            }
374
+
375
+            $this->_host = $reg[3];
376
+            if (isset($reg[5])) {
377
+                $this->_port = $reg[5];
378
+            }
379
+        }
380
+        return $this;
381
+    }
382
+
383
+    /**
384
+     * Returns the path part (possibly an empty string).
385
+     *
386
+     * @return string
387
+     */
388
+    public function getPath()
389
+    {
390
+        return $this->_path;
391
+    }
392
+
393
+    /**
394
+     * Sets the path part (possibly an empty string).
395
+     *
396
+     * @param string $path a path
397
+     *
398
+     * @return $this
399
+     */
400
+    public function setPath($path)
401
+    {
402
+        $this->_path = $path;
403
+        return $this;
404
+    }
405
+
406
+    /**
407
+     * Returns the query string (excluding the leading "?"), or false if "?"
408
+     * is not present in the URL.
409
+     *
410
+     * @return  string|bool
411
+     * @see     self::getQueryVariables()
412
+     */
413
+    public function getQuery()
414
+    {
415
+        return $this->_query;
416
+    }
417
+
418
+    /**
419
+     * Sets the query string (excluding the leading "?"). Specify false if "?"
420
+     * is not present in the URL.
421
+     *
422
+     * @param string|bool $query a query string, e.g. "foo=1&bar=2"
423
+     *
424
+     * @return $this
425
+     * @see    self::setQueryVariables()
426
+     */
427
+    public function setQuery($query)
428
+    {
429
+        $this->_query = $query;
430
+        return $this;
431
+    }
432
+
433
+    /**
434
+     * Returns the fragment name, or false if "#" is not present in the URL.
435
+     *
436
+     * @return  string|bool
437
+     */
438
+    public function getFragment()
439
+    {
440
+        return $this->_fragment;
441
+    }
442
+
443
+    /**
444
+     * Sets the fragment name. Specify false if "#" is not present in the URL.
445
+     *
446
+     * @param string|bool $fragment a fragment excluding the leading "#", or
447
+     *                              false
448
+     *
449
+     * @return $this
450
+     */
451
+    public function setFragment($fragment)
452
+    {
453
+        $this->_fragment = $fragment;
454
+        return $this;
455
+    }
456
+
457
+    /**
458
+     * Returns the query string like an array as the variables would appear in
459
+     * $_GET in a PHP script. If the URL does not contain a "?", an empty array
460
+     * is returned.
461
+     *
462
+     * @return  array
463
+     */
464
+    public function getQueryVariables()
465
+    {
466
+        $pattern = '/[' .
467
+                   preg_quote($this->getOption(self::OPTION_SEPARATOR_INPUT), '/') .
468
+                   ']/';
469
+        $parts   = preg_split($pattern, $this->_query, -1, PREG_SPLIT_NO_EMPTY);
470
+        $return  = array();
471
+
472
+        foreach ($parts as $part) {
473
+            if (strpos($part, '=') !== false) {
474
+                list($key, $value) = explode('=', $part, 2);
475
+            } else {
476
+                $key   = $part;
477
+                $value = null;
478
+            }
479
+
480
+            if ($this->getOption(self::OPTION_ENCODE_KEYS)) {
481
+                $key = rawurldecode($key);
482
+            }
483
+            $value = rawurldecode($value);
484
+
485
+            if ($this->getOption(self::OPTION_USE_BRACKETS) &&
486
+                preg_match('#^(.*)\[([0-9a-z_-]*)\]#i', $key, $matches)) {
487
+
488
+                $key = $matches[1];
489
+                $idx = $matches[2];
490
+
491
+                // Ensure is an array
492
+                if (empty($return[$key]) || !is_array($return[$key])) {
493
+                    $return[$key] = array();
494
+                }
495
+
496
+                // Add data
497
+                if ($idx === '') {
498
+                    $return[$key][] = $value;
499
+                } else {
500
+                    $return[$key][$idx] = $value;
501
+                }
502
+            } elseif (!$this->getOption(self::OPTION_USE_BRACKETS)
503
+                      && !empty($return[$key])
504
+            ) {
505
+                $return[$key]   = (array) $return[$key];
506
+                $return[$key][] = $value;
507
+            } else {
508
+                $return[$key] = $value;
509
+            }
510
+        }
511
+
512
+        return $return;
513
+    }
514
+
515
+    /**
516
+     * Sets the query string to the specified variable in the query string.
517
+     *
518
+     * @param array $array (name => value) array
519
+     *
520
+     * @return $this
521
+     */
522
+    public function setQueryVariables(array $array)
523
+    {
524
+        if (!$array) {
525
+            $this->_query = false;
526
+        } else {
527
+            $this->_query = $this->buildQuery(
528
+                $array,
529
+                $this->getOption(self::OPTION_SEPARATOR_OUTPUT)
530
+            );
531
+        }
532
+        return $this;
533
+    }
534
+
535
+    /**
536
+     * Sets the specified variable in the query string.
537
+     *
538
+     * @param string $name  variable name
539
+     * @param mixed  $value variable value
540
+     *
541
+     * @return $this
542
+     */
543
+    public function setQueryVariable($name, $value)
544
+    {
545
+        $array = $this->getQueryVariables();
546
+        $array[$name] = $value;
547
+        $this->setQueryVariables($array);
548
+        return $this;
549
+    }
550
+
551
+    /**
552
+     * Removes the specifed variable from the query string.
553
+     *
554
+     * @param string $name a query string variable, e.g. "foo" in "?foo=1"
555
+     *
556
+     * @return void
557
+     */
558
+    public function unsetQueryVariable($name)
559
+    {
560
+        $array = $this->getQueryVariables();
561
+        unset($array[$name]);
562
+        $this->setQueryVariables($array);
563
+    }
564
+
565
+    /**
566
+     * Returns a string representation of this URL.
567
+     *
568
+     * @return  string
569
+     */
570
+    public function getURL()
571
+    {
572
+        // See RFC 3986, section 5.3
573
+        $url = "";
574
+
575
+        if ($this->_scheme !== false) {
576
+            $url .= $this->_scheme . ':';
577
+        }
578
+
579
+        $authority = $this->getAuthority();
580
+        if ($authority !== false) {
581
+            $url .= '//' . $authority;
582
+        }
583
+        $url .= $this->_path;
584
+
585
+        if ($this->_query !== false) {
586
+            $url .= '?' . $this->_query;
587
+        }
588
+
589
+        if ($this->_fragment !== false) {
590
+            $url .= '#' . $this->_fragment;
591
+        }
592
+    
593
+        return $url;
594
+    }
595
+
596
+    /**
597
+     * Returns a string representation of this URL.
598
+     *
599
+     * @return  string
600
+     * @see toString()
601
+     */
602
+    public function __toString()
603
+    {
604
+        return $this->getURL();
605
+    }
606
+
607
+    /** 
608
+     * Returns a normalized string representation of this URL. This is useful
609
+     * for comparison of URLs.
610
+     *
611
+     * @return  string
612
+     */
613
+    public function getNormalizedURL()
614
+    {
615
+        $url = clone $this;
616
+        $url->normalize();
617
+        return $url->getUrl();
618
+    }
619
+
620
+    /** 
621
+     * Returns a normalized Net_URL2 instance.
622
+     *
623
+     * @return  Net_URL2
624
+     */
625
+    public function normalize()
626
+    {
627
+        // See RFC 3886, section 6
628
+
629
+        // Schemes are case-insensitive
630
+        if ($this->_scheme) {
631
+            $this->_scheme = strtolower($this->_scheme);
632
+        }
633
+
634
+        // Hostnames are case-insensitive
635
+        if ($this->_host) {
636
+            $this->_host = strtolower($this->_host);
637
+        }
638
+
639
+        // Remove default port number for known schemes (RFC 3986, section 6.2.3)
640
+        if ($this->_port &&
641
+            $this->_scheme &&
642
+            $this->_port == getservbyname($this->_scheme, 'tcp')) {
643
+
644
+            $this->_port = false;
645
+        }
646
+
647
+        // Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1)
648
+        foreach (array('_userinfo', '_host', '_path') as $part) {
649
+            if ($this->$part) {
650
+                $this->$part = preg_replace('/%[0-9a-f]{2}/ie',
651
+                                            'strtoupper("\0")',
652
+                                            $this->$part);
653
+            }
654
+        }
655
+
656
+        // Path segment normalization (RFC 3986, section 6.2.2.3)
657
+        $this->_path = self::removeDotSegments($this->_path);
658
+
659
+        // Scheme based normalization (RFC 3986, section 6.2.3)
660
+        if ($this->_host && !$this->_path) {
661
+            $this->_path = '/';
662
+        }
663
+    }
664
+
665
+    /**
666
+     * Returns whether this instance represents an absolute URL.
667
+     *
668
+     * @return  bool
669
+     */
670
+    public function isAbsolute()
671
+    {
672
+        return (bool) $this->_scheme;
673
+    }
674
+
675
+    /**
676
+     * Returns an Net_URL2 instance representing an absolute URL relative to
677
+     * this URL.
678
+     *
679
+     * @param Net_URL2|string $reference relative URL
680
+     *
681
+     * @return Net_URL2
682
+     */
683
+    public function resolve($reference)
684
+    {
685
+        if (!$reference instanceof Net_URL2) {
686
+            $reference = new self($reference);
687
+        }
688
+        if (!$this->isAbsolute()) {
689
+            throw new Exception('Base-URL must be absolute');
690
+        }
691
+
692
+        // A non-strict parser may ignore a scheme in the reference if it is
693
+        // identical to the base URI's scheme.
694
+        if (!$this->getOption(self::OPTION_STRICT) && $reference->_scheme == $this->_scheme) {
695
+            $reference->_scheme = false;
696
+        }
697
+
698
+        $target = new self('');
699
+        if ($reference->_scheme !== false) {
700
+            $target->_scheme = $reference->_scheme;
701
+            $target->setAuthority($reference->getAuthority());
702
+            $target->_path  = self::removeDotSegments($reference->_path);
703
+            $target->_query = $reference->_query;
704
+        } else {
705
+            $authority = $reference->getAuthority();
706
+            if ($authority !== false) {
707
+                $target->setAuthority($authority);
708
+                $target->_path  = self::removeDotSegments($reference->_path);
709
+                $target->_query = $reference->_query;
710
+            } else {
711
+                if ($reference->_path == '') {
712
+                    $target->_path = $this->_path;
713
+                    if ($reference->_query !== false) {
714
+                        $target->_query = $reference->_query;
715
+                    } else {
716
+                        $target->_query = $this->_query;
717
+                    }
718
+                } else {
719
+                    if (substr($reference->_path, 0, 1) == '/') {
720
+                        $target->_path = self::removeDotSegments($reference->_path);
721
+                    } else {
722
+                        // Merge paths (RFC 3986, section 5.2.3)
723
+                        if ($this->_host !== false && $this->_path == '') {
724
+                            $target->_path = '/' . $this->_path;
725
+                        } else {
726
+                            $i = strrpos($this->_path, '/');
727
+                            if ($i !== false) {
728
+                                $target->_path = substr($this->_path, 0, $i + 1);
729
+                            }
730
+                            $target->_path .= $reference->_path;
731
+                        }
732
+                        $target->_path = self::removeDotSegments($target->_path);
733
+                    }
734
+                    $target->_query = $reference->_query;
735
+                }
736
+                $target->setAuthority($this->getAuthority());
737
+            }
738
+            $target->_scheme = $this->_scheme;
739
+        }
740
+
741
+        $target->_fragment = $reference->_fragment;
742
+
743
+        return $target;
744
+    }
745
+
746
+    /**
747
+     * Removes dots as described in RFC 3986, section 5.2.4, e.g.
748
+     * "/foo/../bar/baz" => "/bar/baz"
749
+     *
750
+     * @param string $path a path
751
+     *
752
+     * @return string a path
753
+     */
754
+    public static function removeDotSegments($path)
755
+    {
756
+        $output = '';
757
+
758
+        // Make sure not to be trapped in an infinite loop due to a bug in this
759
+        // method
760
+        $j = 0;
761
+        while ($path && $j++ < 100) {
762
+            if (substr($path, 0, 2) == './') {
763
+                // Step 2.A
764
+                $path = substr($path, 2);
765
+            } elseif (substr($path, 0, 3) == '../') {
766
+                // Step 2.A
767
+                $path = substr($path, 3);
768
+            } elseif (substr($path, 0, 3) == '/./' || $path == '/.') {
769
+                // Step 2.B
770
+                $path = '/' . substr($path, 3);
771
+            } elseif (substr($path, 0, 4) == '/../' || $path == '/..') {
772
+                // Step 2.C
773
+                $path   = '/' . substr($path, 4);
774
+                $i      = strrpos($output, '/');
775
+                $output = $i === false ? '' : substr($output, 0, $i);
776
+            } elseif ($path == '.' || $path == '..') {
777
+                // Step 2.D
778
+                $path = '';
779
+            } else {
780
+                // Step 2.E
781
+                $i = strpos($path, '/');
782
+                if ($i === 0) {
783
+                    $i = strpos($path, '/', 1);
784
+                }
785
+                if ($i === false) {
786
+                    $i = strlen($path);
787
+                }
788
+                $output .= substr($path, 0, $i);
789
+                $path = substr($path, $i);
790
+            }
791
+        }
792
+
793
+        return $output;
794
+    }
795
+
796
+    /**
797
+     * Percent-encodes all non-alphanumeric characters except these: _ . - ~
798
+     * Similar to PHP's rawurlencode(), except that it also encodes ~ in PHP
799
+     * 5.2.x and earlier.
800
+     *
801
+     * @param  $raw the string to encode
802
+     * @return string
803
+     */
804
+    public static function urlencode($string)
805
+    {
806
+       $encoded = rawurlencode($string);
807
+
808
+        // This is only necessary in PHP < 5.3.
809
+        $encoded = str_replace('%7E', '~', $encoded);
810
+        return $encoded;
811
+    }
812
+
813
+    /**
814
+     * Returns a Net_URL2 instance representing the canonical URL of the
815
+     * currently executing PHP script.
816
+     *
817
+     * @return  string
818
+     */
819
+    public static function getCanonical()
820
+    {
821
+        if (!isset($_SERVER['REQUEST_METHOD'])) {
822
+            // ALERT - no current URL
823
+            throw new Exception('Script was not called through a webserver');
824
+        }
825
+
826
+        // Begin with a relative URL
827
+        $url = new self($_SERVER['PHP_SELF']);
828
+        $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
829
+        $url->_host   = $_SERVER['SERVER_NAME'];
830
+        $port = $_SERVER['SERVER_PORT'];
831
+        if ($url->_scheme == 'http' && $port != 80 ||
832
+            $url->_scheme == 'https' && $port != 443) {
833
+
834
+            $url->_port = $port;
835
+        }
836
+        return $url;
837
+    }
838
+
839
+    /**
840
+     * Returns the URL used to retrieve the current request.
841
+     *
842
+     * @return  string
843
+     */
844
+    public static function getRequestedURL()
845
+    {
846
+        return self::getRequested()->getUrl();
847
+    }
848
+
849
+    /**
850
+     * Returns a Net_URL2 instance representing the URL used to retrieve the
851
+     * current request.
852
+     *
853
+     * @return  Net_URL2
854
+     */
855
+    public static function getRequested()
856
+    {
857
+        if (!isset($_SERVER['REQUEST_METHOD'])) {
858
+            // ALERT - no current URL
859
+            throw new Exception('Script was not called through a webserver');
860
+        }
861
+
862
+        // Begin with a relative URL
863
+        $url = new self($_SERVER['REQUEST_URI']);
864
+        $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
865
+        // Set host and possibly port
866
+        $url->setAuthority($_SERVER['HTTP_HOST']);
867
+        return $url;
868
+    }
869
+
870
+    /**
871
+     * Returns the value of the specified option.
872
+     *
873
+     * @param string $optionName The name of the option to retrieve
874
+     *
875
+     * @return  mixed
876
+     */
877
+    public function getOption($optionName)
878
+    {
879
+        return isset($this->_options[$optionName])
880
+            ? $this->_options[$optionName] : false;
881
+    }
882
+
883
+    /**
884
+     * A simple version of http_build_query in userland. The encoded string is
885
+     * percentage encoded according to RFC 3986.
886
+     *
887
+     * @param array  $data      An array, which has to be converted into
888
+     *                          QUERY_STRING. Anything is possible.
889
+     * @param string $seperator See {@link self::OPTION_SEPARATOR_OUTPUT}
890
+     * @param string $key       For stacked values (arrays in an array).
891
+     *
892
+     * @return string
893
+     */
894
+    protected function buildQuery(array $data, $separator, $key = null)
895
+    {
896
+        $query = array();
897
+        foreach ($data as $name => $value) {
898
+            if ($this->getOption(self::OPTION_ENCODE_KEYS) === true) {
899
+                $name = rawurlencode($name);
900
+            }
901
+            if ($key !== null) {
902
+                if ($this->getOption(self::OPTION_USE_BRACKETS) === true) {
903
+                    $name = $key . '[' . $name . ']';
904
+                } else {
905
+                    $name = $key;
906
+                }
907
+            }
908
+            if (is_array($value)) {
909
+                $query[] = $this->buildQuery($value, $separator, $name);
910
+            } else {
911
+                $query[] = $name . '=' . rawurlencode($value);
912
+            }
913
+        }
914
+        return implode($separator, $query);
915
+    }
916
+
917
+    /**
918
+     * This method uses a funky regex to parse the url into the designated parts.
919
+     *
920
+     * @param string $url
921
+     *
922
+     * @return void
923
+     * @uses   self::$_scheme, self::setAuthority(), self::$_path, self::$_query,
924
+     *         self::$_fragment
925
+     * @see    self::__construct()
926
+     */
927
+    protected function parseUrl($url)
928
+    {
929
+        // The regular expression is copied verbatim from RFC 3986, appendix B.
930
+        // The expression does not validate the URL but matches any string.
931
+        preg_match('!^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?!',
932
+                   $url,
933
+                   $matches);
934
+
935
+        // "path" is always present (possibly as an empty string); the rest
936
+        // are optional.
937
+        $this->_scheme   = !empty($matches[1]) ? $matches[2] : false;
938
+        $this->setAuthority(!empty($matches[3]) ? $matches[4] : false);
939
+        $this->_path     = $matches[5];
940
+        $this->_query    = !empty($matches[6]) ? $matches[7] : false;
941
+        $this->_fragment = !empty($matches[8]) ? $matches[9] : false;
942
+    }
943
+}
944
iRony-0.4.4.tar.gz/lib/FileAPI/ext/PEAR.php Added
1139
 
1
@@ -0,0 +1,1137 @@
2
+<?php
3
+/**
4
+ * PEAR, the PHP Extension and Application Repository
5
+ *
6
+ * PEAR class and PEAR_Error class
7
+ *
8
+ * PHP versions 4 and 5
9
+ *
10
+ * @category   pear
11
+ * @package    PEAR
12
+ * @author     Sterling Hughes <sterling@php.net>
13
+ * @author     Stig Bakken <ssb@php.net>
14
+ * @author     Tomas V.V.Cox <cox@idecnet.com>
15
+ * @author     Greg Beaver <cellog@php.net>
16
+ * @copyright  1997-2009 The Authors
17
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
18
+ * @version    CVS: $Id$
19
+ * @link       http://pear.php.net/package/PEAR
20
+ * @since      File available since Release 0.1
21
+ */
22
+
23
+/**#@+
24
+ * ERROR constants
25
+ */
26
+define('PEAR_ERROR_RETURN',     1);
27
+define('PEAR_ERROR_PRINT',      2);
28
+define('PEAR_ERROR_TRIGGER',    4);
29
+define('PEAR_ERROR_DIE',        8);
30
+define('PEAR_ERROR_CALLBACK',  16);
31
+/**
32
+ * WARNING: obsolete
33
+ * @deprecated
34
+ */
35
+define('PEAR_ERROR_EXCEPTION', 32);
36
+/**#@-*/
37
+define('PEAR_ZE2', (function_exists('version_compare') &&
38
+                    version_compare(zend_version(), "2-dev", "ge")));
39
+
40
+if (substr(PHP_OS, 0, 3) == 'WIN') {
41
+    define('OS_WINDOWS', true);
42
+    define('OS_UNIX',    false);
43
+    define('PEAR_OS',    'Windows');
44
+} else {
45
+    define('OS_WINDOWS', false);
46
+    define('OS_UNIX',    true);
47
+    define('PEAR_OS',    'Unix'); // blatant assumption
48
+}
49
+
50
+$GLOBALS['_PEAR_default_error_mode']     = PEAR_ERROR_RETURN;
51
+$GLOBALS['_PEAR_default_error_options']  = E_USER_NOTICE;
52
+$GLOBALS['_PEAR_destructor_object_list'] = array();
53
+$GLOBALS['_PEAR_shutdown_funcs']         = array();
54
+$GLOBALS['_PEAR_error_handler_stack']    = array();
55
+
56
+@ini_set('track_errors', true);
57
+
58
+/**
59
+ * Base class for other PEAR classes.  Provides rudimentary
60
+ * emulation of destructors.
61
+ *
62
+ * If you want a destructor in your class, inherit PEAR and make a
63
+ * destructor method called _yourclassname (same name as the
64
+ * constructor, but with a "_" prefix).  Also, in your constructor you
65
+ * have to call the PEAR constructor: $this->PEAR();.
66
+ * The destructor method will be called without parameters.  Note that
67
+ * at in some SAPI implementations (such as Apache), any output during
68
+ * the request shutdown (in which destructors are called) seems to be
69
+ * discarded.  If you need to get any debug information from your
70
+ * destructor, use error_log(), syslog() or something similar.
71
+ *
72
+ * IMPORTANT! To use the emulated destructors you need to create the
73
+ * objects by reference: $obj =& new PEAR_child;
74
+ *
75
+ * @category   pear
76
+ * @package    PEAR
77
+ * @author     Stig Bakken <ssb@php.net>
78
+ * @author     Tomas V.V. Cox <cox@idecnet.com>
79
+ * @author     Greg Beaver <cellog@php.net>
80
+ * @copyright  1997-2006 The PHP Group
81
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
82
+ * @version    Release: 1.9.0
83
+ * @link       http://pear.php.net/package/PEAR
84
+ * @see        PEAR_Error
85
+ * @since      Class available since PHP 4.0.2
86
+ * @link        http://pear.php.net/manual/en/core.pear.php#core.pear.pear
87
+ */
88
+class PEAR
89
+{
90
+    // {{{ properties
91
+
92
+    /**
93
+     * Whether to enable internal debug messages.
94
+     *
95
+     * @var     bool
96
+     * @access  private
97
+     */
98
+    var $_debug = false;
99
+
100
+    /**
101
+     * Default error mode for this object.
102
+     *
103
+     * @var     int
104
+     * @access  private
105
+     */
106
+    var $_default_error_mode = null;
107
+
108
+    /**
109
+     * Default error options used for this object when error mode
110
+     * is PEAR_ERROR_TRIGGER.
111
+     *
112
+     * @var     int
113
+     * @access  private
114
+     */
115
+    var $_default_error_options = null;
116
+
117
+    /**
118
+     * Default error handler (callback) for this object, if error mode is
119
+     * PEAR_ERROR_CALLBACK.
120
+     *
121
+     * @var     string
122
+     * @access  private
123
+     */
124
+    var $_default_error_handler = '';
125
+
126
+    /**
127
+     * Which class to use for error objects.
128
+     *
129
+     * @var     string
130
+     * @access  private
131
+     */
132
+    var $_error_class = 'PEAR_Error';
133
+
134
+    /**
135
+     * An array of expected errors.
136
+     *
137
+     * @var     array
138
+     * @access  private
139
+     */
140
+    var $_expected_errors = array();
141
+
142
+    // }}}
143
+
144
+    // {{{ constructor
145
+
146
+    /**
147
+     * Constructor.  Registers this object in
148
+     * $_PEAR_destructor_object_list for destructor emulation if a
149
+     * destructor object exists.
150
+     *
151
+     * @param string $error_class  (optional) which class to use for
152
+     *        error objects, defaults to PEAR_Error.
153
+     * @access public
154
+     * @return void
155
+     */
156
+    function PEAR($error_class = null)
157
+    {
158
+        $classname = strtolower(get_class($this));
159
+        if ($this->_debug) {
160
+            print "PEAR constructor called, class=$classname\n";
161
+        }
162
+        if ($error_class !== null) {
163
+            $this->_error_class = $error_class;
164
+        }
165
+        while ($classname && strcasecmp($classname, "pear")) {
166
+            $destructor = "_$classname";
167
+            if (method_exists($this, $destructor)) {
168
+                global $_PEAR_destructor_object_list;
169
+                $_PEAR_destructor_object_list[] = &$this;
170
+                if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) {
171
+                    register_shutdown_function("_PEAR_call_destructors");
172
+                    $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true;
173
+                }
174
+                break;
175
+            } else {
176
+                $classname = get_parent_class($classname);
177
+            }
178
+        }
179
+    }
180
+
181
+    // }}}
182
+    // {{{ destructor
183
+
184
+    /**
185
+     * Destructor (the emulated type of...).  Does nothing right now,
186
+     * but is included for forward compatibility, so subclass
187
+     * destructors should always call it.
188
+     *
189
+     * See the note in the class desciption about output from
190
+     * destructors.
191
+     *
192
+     * @access public
193
+     * @return void
194
+     */
195
+    function _PEAR() {
196
+        if ($this->_debug) {
197
+            printf("PEAR destructor called, class=%s\n", strtolower(get_class($this)));
198
+        }
199
+    }
200
+
201
+    // }}}
202
+    // {{{ getStaticProperty()
203
+
204
+    /**
205
+    * If you have a class that's mostly/entirely static, and you need static
206
+    * properties, you can use this method to simulate them. Eg. in your method(s)
207
+    * do this: $myVar = &PEAR::getStaticProperty('myclass', 'myVar');
208
+    * You MUST use a reference, or they will not persist!
209
+    *
210
+    * @access public
211
+    * @param  string $class  The calling classname, to prevent clashes
212
+    * @param  string $var    The variable to retrieve.
213
+    * @return mixed   A reference to the variable. If not set it will be
214
+    *                 auto initialised to NULL.
215
+    */
216
+    function &getStaticProperty($class, $var)
217
+    {
218
+        static $properties;
219
+        if (!isset($properties[$class])) {
220
+            $properties[$class] = array();
221
+        }
222
+
223
+        if (!array_key_exists($var, $properties[$class])) {
224
+            $properties[$class][$var] = null;
225
+        }
226
+
227
+        return $properties[$class][$var];
228
+    }
229
+
230
+    // }}}
231
+    // {{{ registerShutdownFunc()
232
+
233
+    /**
234
+    * Use this function to register a shutdown method for static
235
+    * classes.
236
+    *
237
+    * @access public
238
+    * @param  mixed $func  The function name (or array of class/method) to call
239
+    * @param  mixed $args  The arguments to pass to the function
240
+    * @return void
241
+    */
242
+    function registerShutdownFunc($func, $args = array())
243
+    {
244
+        // if we are called statically, there is a potential
245
+        // that no shutdown func is registered.  Bug #6445
246
+        if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) {
247
+            register_shutdown_function("_PEAR_call_destructors");
248
+            $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true;
249
+        }
250
+        $GLOBALS['_PEAR_shutdown_funcs'][] = array($func, $args);
251
+    }
252
+
253
+    // }}}
254
+    // {{{ isError()
255
+
256
+    /**
257
+     * Tell whether a value is a PEAR error.
258
+     *
259
+     * @param   mixed $data   the value to test
260
+     * @param   int   $code   if $data is an error object, return true
261
+     *                        only if $code is a string and
262
+     *                        $obj->getMessage() == $code or
263
+     *                        $code is an integer and $obj->getCode() == $code
264
+     * @access  public
265
+     * @return  bool    true if parameter is an error
266
+     */
267
+    static function isError($data, $code = null)
268
+    {
269
+        if (!is_object($data) || !is_a($data, 'PEAR_Error')) {
270
+            return false;
271
+        }
272
+
273
+        if (is_null($code)) {
274
+            return true;
275
+        } elseif (is_string($code)) {
276
+            return $data->getMessage() == $code;
277
+        }
278
+
279
+        return $data->getCode() == $code;
280
+    }
281
+
282
+    // }}}
283
+    // {{{ setErrorHandling()
284
+
285
+    /**
286
+     * Sets how errors generated by this object should be handled.
287
+     * Can be invoked both in objects and statically.  If called
288
+     * statically, setErrorHandling sets the default behaviour for all
289
+     * PEAR objects.  If called in an object, setErrorHandling sets
290
+     * the default behaviour for that object.
291
+     *
292
+     * @param int $mode
293
+     *        One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT,
294
+     *        PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE,
295
+     *        PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION.
296
+     *
297
+     * @param mixed $options
298
+     *        When $mode is PEAR_ERROR_TRIGGER, this is the error level (one
299
+     *        of E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR).
300
+     *
301
+     *        When $mode is PEAR_ERROR_CALLBACK, this parameter is expected
302
+     *        to be the callback function or method.  A callback
303
+     *        function is a string with the name of the function, a
304
+     *        callback method is an array of two elements: the element
305
+     *        at index 0 is the object, and the element at index 1 is
306
+     *        the name of the method to call in the object.
307
+     *
308
+     *        When $mode is PEAR_ERROR_PRINT or PEAR_ERROR_DIE, this is
309
+     *        a printf format string used when printing the error
310
+     *        message.
311
+     *
312
+     * @access public
313
+     * @return void
314
+     * @see PEAR_ERROR_RETURN
315
+     * @see PEAR_ERROR_PRINT
316
+     * @see PEAR_ERROR_TRIGGER
317
+     * @see PEAR_ERROR_DIE
318
+     * @see PEAR_ERROR_CALLBACK
319
+     * @see PEAR_ERROR_EXCEPTION
320
+     *
321
+     * @since PHP 4.0.5
322
+     */
323
+
324
+    function setErrorHandling($mode = null, $options = null)
325
+    {
326
+        if (isset($this) && is_a($this, 'PEAR')) {
327
+            $setmode     = &$this->_default_error_mode;
328
+            $setoptions  = &$this->_default_error_options;
329
+        } else {
330
+            $setmode     = &$GLOBALS['_PEAR_default_error_mode'];
331
+            $setoptions  = &$GLOBALS['_PEAR_default_error_options'];
332
+        }
333
+
334
+        switch ($mode) {
335
+            case PEAR_ERROR_EXCEPTION:
336
+            case PEAR_ERROR_RETURN:
337
+            case PEAR_ERROR_PRINT:
338
+            case PEAR_ERROR_TRIGGER:
339
+            case PEAR_ERROR_DIE:
340
+            case null:
341
+                $setmode = $mode;
342
+                $setoptions = $options;
343
+                break;
344
+
345
+            case PEAR_ERROR_CALLBACK:
346
+                $setmode = $mode;
347
+                // class/object method callback
348
+                if (is_callable($options)) {
349
+                    $setoptions = $options;
350
+                } else {
351
+                    trigger_error("invalid error callback", E_USER_WARNING);
352
+                }
353
+                break;
354
+
355
+            default:
356
+                trigger_error("invalid error mode", E_USER_WARNING);
357
+                break;
358
+        }
359
+    }
360
+
361
+    // }}}
362
+    // {{{ expectError()
363
+
364
+    /**
365
+     * This method is used to tell which errors you expect to get.
366
+     * Expected errors are always returned with error mode
367
+     * PEAR_ERROR_RETURN.  Expected error codes are stored in a stack,
368
+     * and this method pushes a new element onto it.  The list of
369
+     * expected errors are in effect until they are popped off the
370
+     * stack with the popExpect() method.
371
+     *
372
+     * Note that this method can not be called statically
373
+     *
374
+     * @param mixed $code a single error code or an array of error codes to expect
375
+     *
376
+     * @return int     the new depth of the "expected errors" stack
377
+     * @access public
378
+     */
379
+    function expectError($code = '*')
380
+    {
381
+        if (is_array($code)) {
382
+            array_push($this->_expected_errors, $code);
383
+        } else {
384
+            array_push($this->_expected_errors, array($code));
385
+        }
386
+        return sizeof($this->_expected_errors);
387
+    }
388
+
389
+    // }}}
390
+    // {{{ popExpect()
391
+
392
+    /**
393
+     * This method pops one element off the expected error codes
394
+     * stack.
395
+     *
396
+     * @return array   the list of error codes that were popped
397
+     */
398
+    function popExpect()
399
+    {
400
+        return array_pop($this->_expected_errors);
401
+    }
402
+
403
+    // }}}
404
+    // {{{ _checkDelExpect()
405
+
406
+    /**
407
+     * This method checks unsets an error code if available
408
+     *
409
+     * @param mixed error code
410
+     * @return bool true if the error code was unset, false otherwise
411
+     * @access private
412
+     * @since PHP 4.3.0
413
+     */
414
+    function _checkDelExpect($error_code)
415
+    {
416
+        $deleted = false;
417
+
418
+        foreach ($this->_expected_errors AS $key => $error_array) {
419
+            if (in_array($error_code, $error_array)) {
420
+                unset($this->_expected_errors[$key][array_search($error_code, $error_array)]);
421
+                $deleted = true;
422
+            }
423
+
424
+            // clean up empty arrays
425
+            if (0 == count($this->_expected_errors[$key])) {
426
+                unset($this->_expected_errors[$key]);
427
+            }
428
+        }
429
+        return $deleted;
430
+    }
431
+
432
+    // }}}
433
+    // {{{ delExpect()
434
+
435
+    /**
436
+     * This method deletes all occurences of the specified element from
437
+     * the expected error codes stack.
438
+     *
439
+     * @param  mixed $error_code error code that should be deleted
440
+     * @return mixed list of error codes that were deleted or error
441
+     * @access public
442
+     * @since PHP 4.3.0
443
+     */
444
+    function delExpect($error_code)
445
+    {
446
+        $deleted = false;
447
+        if ((is_array($error_code) && (0 != count($error_code)))) {
448
+            // $error_code is a non-empty array here;
449
+            // we walk through it trying to unset all
450
+            // values
451
+            foreach($error_code as $key => $error) {
452
+                if ($this->_checkDelExpect($error)) {
453
+                    $deleted =  true;
454
+                } else {
455
+                    $deleted = false;
456
+                }
457
+            }
458
+            return $deleted ? true : PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME
459
+        } elseif (!empty($error_code)) {
460
+            // $error_code comes alone, trying to unset it
461
+            if ($this->_checkDelExpect($error_code)) {
462
+                return true;
463
+            } else {
464
+                return PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME
465
+            }
466
+        }
467
+
468
+        // $error_code is empty
469
+        return PEAR::raiseError("The expected error you submitted is empty"); // IMPROVE ME
470
+    }
471
+
472
+    // }}}
473
+    // {{{ raiseError()
474
+
475
+    /**
476
+     * This method is a wrapper that returns an instance of the
477
+     * configured error class with this object's default error
478
+     * handling applied.  If the $mode and $options parameters are not
479
+     * specified, the object's defaults are used.
480
+     *
481
+     * @param mixed $message a text error message or a PEAR error object
482
+     *
483
+     * @param int $code      a numeric error code (it is up to your class
484
+     *                  to define these if you want to use codes)
485
+     *
486
+     * @param int $mode      One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT,
487
+     *                  PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE,
488
+     *                  PEAR_ERROR_CALLBACK, PEAR_ERROR_EXCEPTION.
489
+     *
490
+     * @param mixed $options If $mode is PEAR_ERROR_TRIGGER, this parameter
491
+     *                  specifies the PHP-internal error level (one of
492
+     *                  E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR).
493
+     *                  If $mode is PEAR_ERROR_CALLBACK, this
494
+     *                  parameter specifies the callback function or
495
+     *                  method.  In other error modes this parameter
496
+     *                  is ignored.
497
+     *
498
+     * @param string $userinfo If you need to pass along for example debug
499
+     *                  information, this parameter is meant for that.
500
+     *
501
+     * @param string $error_class The returned error object will be
502
+     *                  instantiated from this class, if specified.
503
+     *
504
+     * @param bool $skipmsg If true, raiseError will only pass error codes,
505
+     *                  the error message parameter will be dropped.
506
+     *
507
+     * @access public
508
+     * @return object   a PEAR error object
509
+     * @see PEAR::setErrorHandling
510
+     * @since PHP 4.0.5
511
+     */
512
+    function &raiseError($message = null,
513
+                         $code = null,
514
+                         $mode = null,
515
+                         $options = null,
516
+                         $userinfo = null,
517
+                         $error_class = null,
518
+                         $skipmsg = false)
519
+    {
520
+        // The error is yet a PEAR error object
521
+        if (is_object($message)) {
522
+            $code        = $message->getCode();
523
+            $userinfo    = $message->getUserInfo();
524
+            $error_class = $message->getType();
525
+            $message->error_message_prefix = '';
526
+            $message     = $message->getMessage();
527
+        }
528
+
529
+        if (isset($this) && isset($this->_expected_errors) && sizeof($this->_expected_errors) > 0 && sizeof($exp = end($this->_expected_errors))) {
530
+            if ($exp[0] == "*" ||
531
+                (is_int(reset($exp)) && in_array($code, $exp)) ||
532
+                (is_string(reset($exp)) && in_array($message, $exp))) {
533
+                $mode = PEAR_ERROR_RETURN;
534
+            }
535
+        }
536
+
537
+        // No mode given, try global ones
538
+        if ($mode === null) {
539
+            // Class error handler
540
+            if (isset($this) && isset($this->_default_error_mode)) {
541
+                $mode    = $this->_default_error_mode;
542
+                $options = $this->_default_error_options;
543
+            // Global error handler
544
+            } elseif (isset($GLOBALS['_PEAR_default_error_mode'])) {
545
+                $mode    = $GLOBALS['_PEAR_default_error_mode'];
546
+                $options = $GLOBALS['_PEAR_default_error_options'];
547
+            }
548
+        }
549
+
550
+        if ($error_class !== null) {
551
+            $ec = $error_class;
552
+        } elseif (isset($this) && isset($this->_error_class)) {
553
+            $ec = $this->_error_class;
554
+        } else {
555
+            $ec = 'PEAR_Error';
556
+        }
557
+
558
+        if (intval(PHP_VERSION) < 5) {
559
+            // little non-eval hack to fix bug #12147
560
+            include 'PEAR/FixPHP5PEARWarnings.php';
561
+            return $a;
562
+        }
563
+
564
+        if ($skipmsg) {
565
+            $a = new $ec($code, $mode, $options, $userinfo);
566
+        } else {
567
+            $a = new $ec($message, $code, $mode, $options, $userinfo);
568
+        }
569
+
570
+        return $a;
571
+    }
572
+
573
+    // }}}
574
+    // {{{ throwError()
575
+
576
+    /**
577
+     * Simpler form of raiseError with fewer options.  In most cases
578
+     * message, code and userinfo are enough.
579
+     *
580
+     * @param string $message
581
+     *
582
+     */
583
+    function &throwError($message = null,
584
+                         $code = null,
585
+                         $userinfo = null)
586
+    {
587
+        if (isset($this) && is_a($this, 'PEAR')) {
588
+            $a = &$this->raiseError($message, $code, null, null, $userinfo);
589
+            return $a;
590
+        }
591
+
592
+        $a = &PEAR::raiseError($message, $code, null, null, $userinfo);
593
+        return $a;
594
+    }
595
+
596
+    // }}}
597
+    function staticPushErrorHandling($mode, $options = null)
598
+    {
599
+        $stack = &$GLOBALS['_PEAR_error_handler_stack'];
600
+        $def_mode    = &$GLOBALS['_PEAR_default_error_mode'];
601
+        $def_options = &$GLOBALS['_PEAR_default_error_options'];
602
+        $stack[] = array($def_mode, $def_options);
603
+        switch ($mode) {
604
+            case PEAR_ERROR_EXCEPTION:
605
+            case PEAR_ERROR_RETURN:
606
+            case PEAR_ERROR_PRINT:
607
+            case PEAR_ERROR_TRIGGER:
608
+            case PEAR_ERROR_DIE:
609
+            case null:
610
+                $def_mode = $mode;
611
+                $def_options = $options;
612
+                break;
613
+
614
+            case PEAR_ERROR_CALLBACK:
615
+                $def_mode = $mode;
616
+                // class/object method callback
617
+                if (is_callable($options)) {
618
+                    $def_options = $options;
619
+                } else {
620
+                    trigger_error("invalid error callback", E_USER_WARNING);
621
+                }
622
+                break;
623
+
624
+            default:
625
+                trigger_error("invalid error mode", E_USER_WARNING);
626
+                break;
627
+        }
628
+        $stack[] = array($mode, $options);
629
+        return true;
630
+    }
631
+
632
+    function staticPopErrorHandling()
633
+    {
634
+        $stack = &$GLOBALS['_PEAR_error_handler_stack'];
635
+        $setmode     = &$GLOBALS['_PEAR_default_error_mode'];
636
+        $setoptions  = &$GLOBALS['_PEAR_default_error_options'];
637
+        array_pop($stack);
638
+        list($mode, $options) = $stack[sizeof($stack) - 1];
639
+        array_pop($stack);
640
+        switch ($mode) {
641
+            case PEAR_ERROR_EXCEPTION:
642
+            case PEAR_ERROR_RETURN:
643
+            case PEAR_ERROR_PRINT:
644
+            case PEAR_ERROR_TRIGGER:
645
+            case PEAR_ERROR_DIE:
646
+            case null:
647
+                $setmode = $mode;
648
+                $setoptions = $options;
649
+                break;
650
+
651
+            case PEAR_ERROR_CALLBACK:
652
+                $setmode = $mode;
653
+                // class/object method callback
654
+                if (is_callable($options)) {
655
+                    $setoptions = $options;
656
+                } else {
657
+                    trigger_error("invalid error callback", E_USER_WARNING);
658
+                }
659
+                break;
660
+
661
+            default:
662
+                trigger_error("invalid error mode", E_USER_WARNING);
663
+                break;
664
+        }
665
+        return true;
666
+    }
667
+
668
+    // {{{ pushErrorHandling()
669
+
670
+    /**
671
+     * Push a new error handler on top of the error handler options stack. With this
672
+     * you can easily override the actual error handler for some code and restore
673
+     * it later with popErrorHandling.
674
+     *
675
+     * @param mixed $mode (same as setErrorHandling)
676
+     * @param mixed $options (same as setErrorHandling)
677
+     *
678
+     * @return bool Always true
679
+     *
680
+     * @see PEAR::setErrorHandling
681
+     */
682
+    function pushErrorHandling($mode, $options = null)
683
+    {
684
+        $stack = &$GLOBALS['_PEAR_error_handler_stack'];
685
+        if (isset($this) && is_a($this, 'PEAR')) {
686
+            $def_mode    = &$this->_default_error_mode;
687
+            $def_options = &$this->_default_error_options;
688
+        } else {
689
+            $def_mode    = &$GLOBALS['_PEAR_default_error_mode'];
690
+            $def_options = &$GLOBALS['_PEAR_default_error_options'];
691
+        }
692
+        $stack[] = array($def_mode, $def_options);
693
+
694
+        if (isset($this) && is_a($this, 'PEAR')) {
695
+            $this->setErrorHandling($mode, $options);
696
+        } else {
697
+            PEAR::setErrorHandling($mode, $options);
698
+        }
699
+        $stack[] = array($mode, $options);
700
+        return true;
701
+    }
702
+
703
+    // }}}
704
+    // {{{ popErrorHandling()
705
+
706
+    /**
707
+    * Pop the last error handler used
708
+    *
709
+    * @return bool Always true
710
+    *
711
+    * @see PEAR::pushErrorHandling
712
+    */
713
+    function popErrorHandling()
714
+    {
715
+        $stack = &$GLOBALS['_PEAR_error_handler_stack'];
716
+        array_pop($stack);
717
+        list($mode, $options) = $stack[sizeof($stack) - 1];
718
+        array_pop($stack);
719
+        if (isset($this) && is_a($this, 'PEAR')) {
720
+            $this->setErrorHandling($mode, $options);
721
+        } else {
722
+            PEAR::setErrorHandling($mode, $options);
723
+        }
724
+        return true;
725
+    }
726
+
727
+    // }}}
728
+    // {{{ loadExtension()
729
+
730
+    /**
731
+    * OS independant PHP extension load. Remember to take care
732
+    * on the correct extension name for case sensitive OSes.
733
+    *
734
+    * @param string $ext The extension name
735
+    * @return bool Success or not on the dl() call
736
+    */
737
+    function loadExtension($ext)
738
+    {
739
+        if (!extension_loaded($ext)) {
740
+            // if either returns true dl() will produce a FATAL error, stop that
741
+            if ((ini_get('enable_dl') != 1) || (ini_get('safe_mode') == 1)) {
742
+                return false;
743
+            }
744
+
745
+            if (OS_WINDOWS) {
746
+                $suffix = '.dll';
747
+            } elseif (PHP_OS == 'HP-UX') {
748
+                $suffix = '.sl';
749
+            } elseif (PHP_OS == 'AIX') {
750
+                $suffix = '.a';
751
+            } elseif (PHP_OS == 'OSX') {
752
+                $suffix = '.bundle';
753
+            } else {
754
+                $suffix = '.so';
755
+            }
756
+
757
+            return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix);
758
+        }
759
+
760
+        return true;
761
+    }
762
+
763
+    // }}}
764
+}
765
+
766
+if (PEAR_ZE2) {
767
+    include_once 'PEAR5.php';
768
+}
769
+
770
+// {{{ _PEAR_call_destructors()
771
+
772
+function _PEAR_call_destructors()
773
+{
774
+    global $_PEAR_destructor_object_list;
775
+    if (is_array($_PEAR_destructor_object_list) &&
776
+        sizeof($_PEAR_destructor_object_list))
777
+    {
778
+        reset($_PEAR_destructor_object_list);
779
+        if (PEAR_ZE2) {
780
+            $destructLifoExists = PEAR5::getStaticProperty('PEAR', 'destructlifo');
781
+        } else {
782
+            $destructLifoExists = PEAR::getStaticProperty('PEAR', 'destructlifo');
783
+        }
784
+
785
+        if ($destructLifoExists) {
786
+            $_PEAR_destructor_object_list = array_reverse($_PEAR_destructor_object_list);
787
+        }
788
+
789
+        while (list($k, $objref) = each($_PEAR_destructor_object_list)) {
790
+            $classname = get_class($objref);
791
+            while ($classname) {
792
+                $destructor = "_$classname";
793
+                if (method_exists($objref, $destructor)) {
794
+                    $objref->$destructor();
795
+                    break;
796
+                } else {
797
+                    $classname = get_parent_class($classname);
798
+                }
799
+            }
800
+        }
801
+        // Empty the object list to ensure that destructors are
802
+        // not called more than once.
803
+        $_PEAR_destructor_object_list = array();
804
+    }
805
+
806
+    // Now call the shutdown functions
807
+    if (isset($GLOBALS['_PEAR_shutdown_funcs']) AND is_array($GLOBALS['_PEAR_shutdown_funcs']) AND !empty($GLOBALS['_PEAR_shutdown_funcs'])) {
808
+        foreach ($GLOBALS['_PEAR_shutdown_funcs'] as $value) {
809
+            call_user_func_array($value[0], $value[1]);
810
+        }
811
+    }
812
+}
813
+
814
+// }}}
815
+/**
816
+ * Standard PEAR error class for PHP 4
817
+ *
818
+ * This class is supserseded by {@link PEAR_Exception} in PHP 5
819
+ *
820
+ * @category   pear
821
+ * @package    PEAR
822
+ * @author     Stig Bakken <ssb@php.net>
823
+ * @author     Tomas V.V. Cox <cox@idecnet.com>
824
+ * @author     Gregory Beaver <cellog@php.net>
825
+ * @copyright  1997-2006 The PHP Group
826
+ * @license    http://opensource.org/licenses/bsd-license.php New BSD License
827
+ * @version    Release: 1.9.0
828
+ * @link       http://pear.php.net/manual/en/core.pear.pear-error.php
829
+ * @see        PEAR::raiseError(), PEAR::throwError()
830
+ * @since      Class available since PHP 4.0.2
831
+ */
832
+class PEAR_Error
833
+{
834
+    // {{{ properties
835
+
836
+    var $error_message_prefix = '';
837
+    var $mode                 = PEAR_ERROR_RETURN;
838
+    var $level                = E_USER_NOTICE;
839
+    var $code                 = -1;
840
+    var $message              = '';
841
+    var $userinfo             = '';
842
+    var $backtrace            = null;
843
+
844
+    // }}}
845
+    // {{{ constructor
846
+
847
+    /**
848
+     * PEAR_Error constructor
849
+     *
850
+     * @param string $message  message
851
+     *
852
+     * @param int $code     (optional) error code
853
+     *
854
+     * @param int $mode     (optional) error mode, one of: PEAR_ERROR_RETURN,
855
+     * PEAR_ERROR_PRINT, PEAR_ERROR_DIE, PEAR_ERROR_TRIGGER,
856
+     * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION
857
+     *
858
+     * @param mixed $options   (optional) error level, _OR_ in the case of
859
+     * PEAR_ERROR_CALLBACK, the callback function or object/method
860
+     * tuple.
861
+     *
862
+     * @param string $userinfo (optional) additional user/debug info
863
+     *
864
+     * @access public
865
+     *
866
+     */
867
+    function PEAR_Error($message = 'unknown error', $code = null,
868
+                        $mode = null, $options = null, $userinfo = null)
869
+    {
870
+        if ($mode === null) {
871
+            $mode = PEAR_ERROR_RETURN;
872
+        }
873
+        $this->message   = $message;
874
+        $this->code      = $code;
875
+        $this->mode      = $mode;
876
+        $this->userinfo  = $userinfo;
877
+
878
+        if (PEAR_ZE2) {
879
+            $skiptrace = PEAR5::getStaticProperty('PEAR_Error', 'skiptrace');
880
+        } else {
881
+            $skiptrace = PEAR::getStaticProperty('PEAR_Error', 'skiptrace');
882
+        }
883
+
884
+        if (!$skiptrace) {
885
+            $this->backtrace = debug_backtrace();
886
+            if (isset($this->backtrace[0]) && isset($this->backtrace[0]['object'])) {
887
+                unset($this->backtrace[0]['object']);
888
+            }
889
+        }
890
+
891
+        if ($mode & PEAR_ERROR_CALLBACK) {
892
+            $this->level = E_USER_NOTICE;
893
+            $this->callback = $options;
894
+        } else {
895
+            if ($options === null) {
896
+                $options = E_USER_NOTICE;
897
+            }
898
+
899
+            $this->level = $options;
900
+            $this->callback = null;
901
+        }
902
+
903
+        if ($this->mode & PEAR_ERROR_PRINT) {
904
+            if (is_null($options) || is_int($options)) {
905
+                $format = "%s";
906
+            } else {
907
+                $format = $options;
908
+            }
909
+
910
+            printf($format, $this->getMessage());
911
+        }
912
+
913
+        if ($this->mode & PEAR_ERROR_TRIGGER) {
914
+            trigger_error($this->getMessage(), $this->level);
915
+        }
916
+
917
+        if ($this->mode & PEAR_ERROR_DIE) {
918
+            $msg = $this->getMessage();
919
+            if (is_null($options) || is_int($options)) {
920
+                $format = "%s";
921
+                if (substr($msg, -1) != "\n") {
922
+                    $msg .= "\n";
923
+                }
924
+            } else {
925
+                $format = $options;
926
+            }
927
+            die(sprintf($format, $msg));
928
+        }
929
+
930
+        if ($this->mode & PEAR_ERROR_CALLBACK) {
931
+            if (is_callable($this->callback)) {
932
+                call_user_func($this->callback, $this);
933
+            }
934
+        }
935
+
936
+        if ($this->mode & PEAR_ERROR_EXCEPTION) {
937
+            trigger_error("PEAR_ERROR_EXCEPTION is obsolete, use class PEAR_Exception for exceptions", E_USER_WARNING);
938
+            eval('$e = new Exception($this->message, $this->code);throw($e);');
939
+        }
940
+    }
941
+
942
+    // }}}
943
+    // {{{ getMode()
944
+
945
+    /**
946
+     * Get the error mode from an error object.
947
+     *
948
+     * @return int error mode
949
+     * @access public
950
+     */
951
+    function getMode() {
952
+        return $this->mode;
953
+    }
954
+
955
+    // }}}
956
+    // {{{ getCallback()
957
+
958
+    /**
959
+     * Get the callback function/method from an error object.
960
+     *
961
+     * @return mixed callback function or object/method array
962
+     * @access public
963
+     */
964
+    function getCallback() {
965
+        return $this->callback;
966
+    }
967
+
968
+    // }}}
969
+    // {{{ getMessage()
970
+
971
+
972
+    /**
973
+     * Get the error message from an error object.
974
+     *
975
+     * @return  string  full error message
976
+     * @access public
977
+     */
978
+    function getMessage()
979
+    {
980
+        return ($this->error_message_prefix . $this->message);
981
+    }
982
+
983
+
984
+    // }}}
985
+    // {{{ getCode()
986
+
987
+    /**
988
+     * Get error code from an error object
989
+     *
990
+     * @return int error code
991
+     * @access public
992
+     */
993
+     function getCode()
994
+     {
995
+        return $this->code;
996
+     }
997
+
998
+    // }}}
999
+    // {{{ getType()
1000
+
1001
+    /**
1002
+     * Get the name of this error/exception.
1003
+     *
1004
+     * @return string error/exception name (type)
1005
+     * @access public
1006
+     */
1007
+    function getType()
1008
+    {
1009
+        return get_class($this);
1010
+    }
1011
+
1012
+    // }}}
1013
+    // {{{ getUserInfo()
1014
+
1015
+    /**
1016
+     * Get additional user-supplied information.
1017
+     *
1018
+     * @return string user-supplied information
1019
+     * @access public
1020
+     */
1021
+    function getUserInfo()
1022
+    {
1023
+        return $this->userinfo;
1024
+    }
1025
+
1026
+    // }}}
1027
+    // {{{ getDebugInfo()
1028
+
1029
+    /**
1030
+     * Get additional debug information supplied by the application.
1031
+     *
1032
+     * @return string debug information
1033
+     * @access public
1034
+     */
1035
+    function getDebugInfo()
1036
+    {
1037
+        return $this->getUserInfo();
1038
+    }
1039
+
1040
+    // }}}
1041
+    // {{{ getBacktrace()
1042
+
1043
+    /**
1044
+     * Get the call backtrace from where the error was generated.
1045
+     * Supported with PHP 4.3.0 or newer.
1046
+     *
1047
+     * @param int $frame (optional) what frame to fetch
1048
+     * @return array Backtrace, or NULL if not available.
1049
+     * @access public
1050
+     */
1051
+    function getBacktrace($frame = null)
1052
+    {
1053
+        if (defined('PEAR_IGNORE_BACKTRACE')) {
1054
+            return null;
1055
+        }
1056
+        if ($frame === null) {
1057
+            return $this->backtrace;
1058
+        }
1059
+        return $this->backtrace[$frame];
1060
+    }
1061
+
1062
+    // }}}
1063
+    // {{{ addUserInfo()
1064
+
1065
+    function addUserInfo($info)
1066
+    {
1067
+        if (empty($this->userinfo)) {
1068
+            $this->userinfo = $info;
1069
+        } else {
1070
+            $this->userinfo .= " ** $info";
1071
+        }
1072
+    }
1073
+
1074
+    // }}}
1075
+    // {{{ toString()
1076
+    function __toString()
1077
+    {
1078
+        return $this->getMessage();
1079
+    }
1080
+    // }}}
1081
+    // {{{ toString()
1082
+
1083
+    /**
1084
+     * Make a string representation of this object.
1085
+     *
1086
+     * @return string a string with an object summary
1087
+     * @access public
1088
+     */
1089
+    function toString() {
1090
+        $modes = array();
1091
+        $levels = array(E_USER_NOTICE  => 'notice',
1092
+                        E_USER_WARNING => 'warning',
1093
+                        E_USER_ERROR   => 'error');
1094
+        if ($this->mode & PEAR_ERROR_CALLBACK) {
1095
+            if (is_array($this->callback)) {
1096
+                $callback = (is_object($this->callback[0]) ?
1097
+                    strtolower(get_class($this->callback[0])) :
1098
+                    $this->callback[0]) . '::' .
1099
+                    $this->callback[1];
1100
+            } else {
1101
+                $callback = $this->callback;
1102
+            }
1103
+            return sprintf('[%s: message="%s" code=%d mode=callback '.
1104
+                           'callback=%s prefix="%s" info="%s"]',
1105
+                           strtolower(get_class($this)), $this->message, $this->code,
1106
+                           $callback, $this->error_message_prefix,
1107
+                           $this->userinfo);
1108
+        }
1109
+        if ($this->mode & PEAR_ERROR_PRINT) {
1110
+            $modes[] = 'print';
1111
+        }
1112
+        if ($this->mode & PEAR_ERROR_TRIGGER) {
1113
+            $modes[] = 'trigger';
1114
+        }
1115
+        if ($this->mode & PEAR_ERROR_DIE) {
1116
+            $modes[] = 'die';
1117
+        }
1118
+        if ($this->mode & PEAR_ERROR_RETURN) {
1119
+            $modes[] = 'return';
1120
+        }
1121
+        return sprintf('[%s: message="%s" code=%d mode=%s level=%s '.
1122
+                       'prefix="%s" info="%s"]',
1123
+                       strtolower(get_class($this)), $this->message, $this->code,
1124
+                       implode("|", $modes), $levels[$this->level],
1125
+                       $this->error_message_prefix,
1126
+                       $this->userinfo);
1127
+    }
1128
+
1129
+    // }}}
1130
+}
1131
+
1132
+/*
1133
+ * Local Variables:
1134
+ * mode: php
1135
+ * tab-width: 4
1136
+ * c-basic-offset: 4
1137
+ * End:
1138
+ */
1139
iRony-0.4.4.tar.gz/lib/FileAPI/ext/PEAR5.php Added
36
 
1
@@ -0,0 +1,33 @@
2
+<?php
3
+/**
4
+ * This is only meant for PHP 5 to get rid of certain strict warning
5
+ * that doesn't get hidden since it's in the shutdown function
6
+ */
7
+class PEAR5
8
+{
9
+    /**
10
+    * If you have a class that's mostly/entirely static, and you need static
11
+    * properties, you can use this method to simulate them. Eg. in your method(s)
12
+    * do this: $myVar = &PEAR5::getStaticProperty('myclass', 'myVar');
13
+    * You MUST use a reference, or they will not persist!
14
+    *
15
+    * @access public
16
+    * @param  string $class  The calling classname, to prevent clashes
17
+    * @param  string $var    The variable to retrieve.
18
+    * @return mixed   A reference to the variable. If not set it will be
19
+    *                 auto initialised to NULL.
20
+    */
21
+    static function &getStaticProperty($class, $var)
22
+    {
23
+        static $properties;
24
+        if (!isset($properties[$class])) {
25
+            $properties[$class] = array();
26
+        }
27
+
28
+        if (!array_key_exists($var, $properties[$class])) {
29
+            $properties[$class][$var] = null;
30
+        }
31
+
32
+        return $properties[$class][$var];
33
+    }
34
+}
35
\ No newline at end of file
36
iRony-0.4.3.tar.gz/lib/FileAPI/file_api.php -> iRony-0.4.4.tar.gz/lib/FileAPI/file_api.php Changed
456
 
1
@@ -3,7 +3,7 @@
2
  +--------------------------------------------------------------------------+
3
  | This file is part of the Kolab File API                                  |
4
  |                                                                          |
5
- | Copyright (C) 2012-2015, Kolab Systems AG                                |
6
+ | Copyright (C) 2012-2013, Kolab Systems AG                                |
7
  |                                                                          |
8
  | This program is free software: you can redistribute it and/or modify     |
9
  | it under the terms of the GNU Affero General Public License as published |
10
@@ -25,31 +25,25 @@
11
 class file_api extends file_api_core
12
 {
13
     public $session;
14
-    public $config;
15
-    public $browser;
16
     public $output_type = file_api_core::OUTPUT_JSON;
17
 
18
+    private $conf;
19
+    private $browser;
20
 
21
-    /**
22
-     * Class factory.
23
-     */
24
-    public static function factory()
25
-    {
26
-        $class = 'file_api' . (!empty($_GET['wopi']) ? '_wopi' : '');
27
-
28
-        return new $class;
29
-    }
30
 
31
-    /**
32
-     * Class constructor.
33
-     */
34
     public function __construct()
35
     {
36
         $rcube = rcube::get_instance();
37
-        register_shutdown_function(array($this, 'shutdown'));
38
+        $rcube->add_shutdown_function(array($this, 'shutdown'));
39
 
40
-        $this->config = $rcube->config;
41
+        $this->conf = $rcube->config;
42
         $this->session_init();
43
+
44
+        if ($_SESSION['config']) {
45
+            $this->config = $_SESSION['config'];
46
+        }
47
+
48
+        $this->locale_init();
49
     }
50
 
51
     /**
52
@@ -60,37 +54,30 @@
53
         $this->request = strtolower($_GET['method']);
54
 
55
         // Check the session, authenticate the user
56
-        if (!$this->session_validate($this->request == 'authenticate', $_REQUEST['token'])) {
57
+        if (!$this->session_validate()) {
58
             $this->session->destroy(session_id());
59
-            $this->session->regenerate_id(false);
60
 
61
-            if ($username = $this->authenticate()) {
62
-                // Init locale after the session started
63
-                $this->locale_init();
64
-                $this->env['language'] = $this->language;
65
+            if ($this->request == 'authenticate') {
66
+                $this->session->regenerate_id(false);
67
 
68
-                $_SESSION['user'] = $username;
69
-                $_SESSION['env']  = $this->env;
70
+                if ($username = $this->authenticate()) {
71
+                    $_SESSION['user']   = $username;
72
+                    $_SESSION['time']   = time();
73
+                    $_SESSION['config'] = $this->config;
74
 
75
-                // remember client API version
76
-                if (is_numeric($_GET['version'])) {
77
-                    $_SESSION['version'] = $_GET['version'];
78
-                }
79
+                    // remember client API version
80
+                    if (is_numeric($_GET['version'])) {
81
+                        $_SESSION['version'] = $_GET['version'];
82
+                    }
83
 
84
-                if ($this->request == 'authenticate') {
85
                     $this->output_success(array(
86
                         'token'        => session_id(),
87
                         'capabilities' => $this->capabilities(),
88
                     ));
89
                 }
90
             }
91
-            else {
92
-                throw new Exception("Invalid session", file_api_core::ERROR_UNAUTHORIZED);
93
-            }
94
-        }
95
-        else {
96
-            // Init locale after the session started
97
-            $this->locale_init();
98
+
99
+            throw new Exception("Invalid session", 403);
100
         }
101
 
102
         // Call service method
103
@@ -104,35 +91,28 @@
104
     /**
105
      * Session validation check and session start
106
      */
107
-    protected function session_validate($new_session = false, $token = null)
108
+    private function session_validate()
109
     {
110
-        if (!$new_session) {
111
-            $sess_id = rcube_utils::request_header('X-Session-Token') ?: $token;
112
-        }
113
+        $sess_id = rcube_utils::request_header('X-Session-Token') ?: $_REQUEST['token'];
114
 
115
         if (empty($sess_id)) {
116
-            $this->session->start();
117
+            session_start();
118
             return false;
119
         }
120
 
121
         session_id($sess_id);
122
-        $this->session->start();
123
+        session_start();
124
 
125
         if (empty($_SESSION['user'])) {
126
             return false;
127
         }
128
 
129
-        // Single-document session?
130
-        if (!($this instanceof file_api_wopi)
131
-            && ($doc_id = $_SESSION['document_session'])
132
-            && (strpos($this->request, 'document') !== 0 || $doc_id != $_GET['id'])
133
-        ) {
134
-            throw new Exception("Access denied", file_api_core::ERROR_UNAUTHORIZED);
135
-        }
136
-
137
-        if ($_SESSION['env']) {
138
-            $this->env = $_SESSION['env'];
139
+        $timeout = $this->conf->get('session_lifetime', 0) * 60;
140
+        if ($timeout && $_SESSION['time'] && $_SESSION['time'] < time() - $timeout) {
141
+            return false;
142
         }
143
+        // update session time
144
+        $_SESSION['time'] = time();
145
 
146
         return true;
147
     }
148
@@ -140,11 +120,11 @@
149
     /**
150
      * Initializes session
151
      */
152
-    protected function session_init()
153
+    private function session_init()
154
     {
155
         $rcube     = rcube::get_instance();
156
-        $sess_name = $this->config->get('session_name');
157
-        $lifetime  = $this->config->get('session_lifetime', 0) * 60;
158
+        $sess_name = $this->conf->get('session_name');
159
+        $lifetime  = $this->conf->get('session_lifetime', 0) * 60;
160
 
161
         if ($lifetime) {
162
             ini_set('session.gc_maxlifetime', $lifetime * 2);
163
@@ -154,19 +134,14 @@
164
         ini_set('session.use_cookies', 0);
165
         ini_set('session.serialize_handler', 'php');
166
 
167
-        // Roundcube Framework >= 1.2
168
-        if (in_array('factory', get_class_methods('rcube_session'))) {
169
-            $this->session = rcube_session::factory($this->config);
170
-        }
171
-        // Rouncube Framework < 1.2
172
-        else {
173
-            $this->session = new rcube_session($rcube->get_dbh(), $this->config);
174
-            $this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
175
-            $this->session->set_ip_check($this->config->get('ip_check'));
176
-        }
177
+        // use database for storing session data
178
+        $this->session = new rcube_session($rcube->get_dbh(), $this->conf);
179
 
180
         $this->session->register_gc_handler(array($rcube, 'gc'));
181
 
182
+        $this->session->set_secret($this->conf->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
183
+        $this->session->set_ip_check($this->conf->get('ip_check'));
184
+
185
         // this is needed to correctly close session in shutdown function
186
         $rcube->session = $this->session;
187
     }
188
@@ -177,29 +152,13 @@
189
     public function shutdown()
190
     {
191
         // write performance stats to logs/console
192
-        if ($this->config->get('devel_mode') || $this->config->get('performance_stats')) {
193
-            // we have to disable per_user_logging to make sure stats end up in the main console log
194
-            $this->config->set('per_user_logging', false);
195
-
196
-            // make sure logged numbers use unified format
197
-            setlocale(LC_NUMERIC, 'en_US.utf8', 'en_US.UTF-8', 'en_US', 'C');
198
-
199
-            if (function_exists('memory_get_usage')) {
200
-                $mem = round(memory_get_usage() / 1024 /1024, 1);
201
-            }
202
-            if (function_exists('memory_get_peak_usage')) {
203
-                $mem .= '/'. round(memory_get_peak_usage() / 1024 / 1024, 1);
204
-            }
205
-
206
-            $path    = !empty($this->path) ? '/' . implode($this->path, '/') : '';
207
-            $request = ($this instanceof file_api_wopi ? 'wopi/' : '') . $this->request;
208
-
209
-            if ($path !== '' && substr_compare($this->request, $path, -1 * strlen($path), strlen($path), true) != 0) {
210
-                $request .= $path;
211
-            }
212
-
213
-            $log = sprintf('%s: %s [%s]', $this->method ?: $_SERVER['REQUEST_METHOD'], trim($request) ?: '/', $mem);
214
+        if ($this->conf->get('devel_mode')) {
215
+            if (function_exists('memory_get_peak_usage'))
216
+                $mem = memory_get_peak_usage();
217
+            else if (function_exists('memory_get_usage'))
218
+                $mem = memory_get_usage();
219
 
220
+            $log = trim($this->request . ($mem ? sprintf(' [%.1f MB]', $mem/1024/1024) : ''));
221
             if (defined('FILE_API_START')) {
222
                 rcube::print_timer(FILE_API_START, $log);
223
             }
224
@@ -212,7 +171,7 @@
225
     /**
226
      * Authentication request handler (HTTP Auth)
227
      */
228
-    protected function authenticate()
229
+    private function authenticate()
230
     {
231
         if (isset($_POST['username'])) {
232
             $username = $_POST['username'];
233
@@ -224,44 +183,37 @@
234
         }
235
         // when used with (f)cgi no PHP_AUTH* variables are available without defining a special rewrite rule
236
         else if (!isset($_SERVER['PHP_AUTH_USER'])) {
237
-            $tokens = array(
238
-                $_SERVER['REMOTE_USER'],
239
-                $_SERVER['REDIRECT_REMOTE_USER'],
240
-                $_SERVER['HTTP_AUTHORIZATION'],
241
-                rcube_utils::request_header('Authorization'),
242
-            );
243
+            // "Basic didhfiefdhfu4fjfjdsa34drsdfterrde..."
244
+            if (isset($_SERVER["REMOTE_USER"])) {
245
+                $basicAuthData = base64_decode(substr($_SERVER["REMOTE_USER"], 6));
246
+            }
247
+            else if (isset($_SERVER["REDIRECT_REMOTE_USER"])) {
248
+                $basicAuthData = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"], 6));
249
+            }
250
+            else if (isset($_SERVER["Authorization"])) {
251
+                $basicAuthData = base64_decode(substr($_SERVER["Authorization"], 6));
252
+            }
253
+            else if (isset($_SERVER["HTTP_AUTHORIZATION"])) {
254
+                $basicAuthData = base64_decode(substr($_SERVER["HTTP_AUTHORIZATION"], 6));
255
+            }
256
 
257
-            foreach ($tokens as $token) {
258
-                if (!empty($token)) {
259
-                    if (stripos($token, 'Basic ') === 0) {
260
-                        $basicAuthData = base64_decode(substr($token, 6));
261
-                        list($username, $password) = explode(':', $basicAuthData, 2);
262
-                        if ($username) {
263
-                            break;
264
-                        }
265
-                    }
266
-                    else if (stripos($token, 'Bearer ') === 0) {
267
-                        $username = base64_decode(substr($token, 7));
268
-                        if ($username) {
269
-                            break;
270
-                        }
271
-                    }
272
-                }
273
+            if (isset($basicAuthData) && !empty($basicAuthData)) {
274
+                list($username, $password) = explode(":", $basicAuthData);
275
             }
276
         }
277
 
278
         if (!empty($username)) {
279
             $backend = $this->get_backend();
280
             $result  = $backend->authenticate($username, $password);
281
+        }
282
 
283
-            if (empty($result)) {
284
+        if (empty($result)) {
285
 /*
286
-                header('WWW-Authenticate: Basic realm="' . $this->app_name .'"');
287
-                header('HTTP/1.1 401 Unauthorized');
288
-                exit;
289
+            header('WWW-Authenticate: Basic realm="' . $this->app_name .'"');
290
+            header('HTTP/1.1 401 Unauthorized');
291
+            exit;
292
 */
293
-                throw new Exception("Invalid password or username", file_api_core::ERROR_UNAUTHORIZED);
294
-            }
295
+            throw new Exception("Invalid password or username", file_api_core::ERROR_CODE);
296
         }
297
 
298
         return $username;
299
@@ -270,7 +222,7 @@
300
     /**
301
      * Storage/System method handler
302
      */
303
-    protected function request_handler($request)
304
+    private function request_handler($request)
305
     {
306
         // handle "global" requests that don't require api driver
307
         switch ($request) {
308
@@ -282,14 +234,14 @@
309
                 return array();
310
 
311
             case 'configure':
312
-                foreach (array_keys($this->env) as $name) {
313
+                foreach (array_keys($this->config) as $name) {
314
                     if (isset($_GET[$name])) {
315
-                        $this->env[$name] = $_GET[$name];
316
+                        $this->config[$name] = $_GET[$name];
317
                     }
318
                 }
319
-                $_SESSION['env'] = $this->env;
320
+                $_SESSION['config'] = $this->config;
321
 
322
-                return $this->env;
323
+                return $this->config;
324
 
325
             case 'upload_progress':
326
                 return $this->upload_progress();
327
@@ -303,18 +255,13 @@
328
 
329
         // handle request
330
         if ($request && preg_match('/^[a-z0-9_-]+$/', $request)) {
331
+            // request name aliases for backward compatibility
332
             $aliases = array(
333
-                // request name aliases for backward compatibility
334
                 'lock'          => 'lock_create',
335
                 'unlock'        => 'lock_delete',
336
                 'folder_rename' => 'folder_move',
337
             );
338
 
339
-            // Redirect all document_* actions into 'document' action
340
-            if (preg_match('/^(sessions|invitations|document_[a-z]+)$/', $request)) {
341
-                $request = 'document';
342
-            }
343
-
344
             $request = $aliases[$request] ?: $request;
345
 
346
             require_once __DIR__ . "/api/common.php";
347
@@ -343,7 +290,7 @@
348
             if (!empty($status)) {
349
                 $status['percent'] = round($status['current']/$status['total']*100);
350
                 if ($status['percent'] < 100) {
351
-                    $diff = max(1, time() - intval($status['start_time']));
352
+                    $diff = time() - intval($status['start_time']);
353
                     // calculate time to end of uploading (in seconds)
354
                     $status['eta'] = intval($diff * (100 - $status['percent']) / $status['percent']);
355
                     // average speed (bytes per second)
356
@@ -368,44 +315,12 @@
357
      */
358
     public function file_url($file)
359
     {
360
-        return $this->api_url() . '?method=file_get'
361
+        return file_utils::script_uri(). '?method=file_get'
362
             . '&file=' . urlencode($file)
363
             . '&token=' . urlencode(session_id());
364
     }
365
 
366
     /**
367
-     * Returns API URL
368
-     *
369
-     * @return string API URL
370
-     */
371
-    public function api_url()
372
-    {
373
-        $api_url = $this->config->get('file_api_url', '');
374
-
375
-        if (!preg_match('|^https?://|', $api_url)) {
376
-            $schema = rcube_utils::https_check() ? 'https' : 'http';
377
-            $port   = $schema == 'http' ? 80 : 443;
378
-            $url    = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
379
-
380
-            if ($_SERVER['SERVER_PORT'] != $port && $_SERVER['SERVER_PORT'] != 80) {
381
-                $url .= ':' . $_SERVER['SERVER_PORT'];
382
-            }
383
-
384
-            if ($api_url) {
385
-                $api_url = $url . '/' . trim($api_url, '/ ');
386
-            }
387
-            else {
388
-                $url .= preg_replace('/\/?\?.*$/', '', $_SERVER['REQUEST_URI']);
389
-                $url = preg_replace('/\/api$/', '', $url);
390
-
391
-                $api_url = $url . '/api';
392
-            }
393
-        }
394
-
395
-        return rtrim($api_url, '/ ');
396
-    }
397
-
398
-    /**
399
      * Returns web browser object
400
      *
401
      * @return rcube_browser Web browser object
402
@@ -459,22 +374,12 @@
403
 
404
         if (!empty($_REQUEST['req_id'])) {
405
             $response['req_id'] = $_REQUEST['req_id'];
406
-            header("X-Chwala-Request-ID: " . $_REQUEST['req_id']);
407
         }
408
 
409
         if (empty($response['code'])) {
410
             $response['code'] = file_api_core::ERROR_CODE;
411
         }
412
 
413
-        header("X-Chwala-Error: " . $response['code']);
414
-
415
-        // When binary response is expected return real
416
-        // HTTP error instaead of JSON response with code 200
417
-        if ($this->is_binary_request()) {
418
-            header(sprintf("HTTP/1.0 %d %s", $response['code'], $response ?: "Server error"));
419
-            exit;
420
-        }
421
-
422
         $this->output_send($response);
423
     }
424
 
425
@@ -483,27 +388,15 @@
426
      *
427
      * @param mixed $data Data
428
      */
429
-    public function output_send($data = null)
430
+    protected function output_send($data)
431
     {
432
         // Send response
433
-        if ($data !== null) {
434
-            header("Content-Type: {$this->output_type}; charset=utf-8");
435
-            echo rcube_output::json_serialize($data);
436
-        }
437
-
438
+        header("Content-Type: {$this->output_type}; charset=utf-8");
439
+        echo json_encode($data);
440
         exit;
441
     }
442
 
443
     /**
444
-     * Find out if current request expects binary output
445
-     */
446
-    protected function is_binary_request()
447
-    {
448
-        return $_SERVER['REQUEST_METHOD'] == 'GET' &&
449
-            ($this->request == 'file_get' || $this->request == 'document');
450
-    }
451
-
452
-    /**
453
      * Returns API version supported by the client
454
      */
455
     public function client_version()
456
iRony-0.4.3.tar.gz/lib/FileAPI/file_api_core.php -> iRony-0.4.4.tar.gz/lib/FileAPI/file_api_core.php Changed
289
 
1
@@ -24,27 +24,21 @@
2
 
3
 class file_api_core extends file_locale
4
 {
5
-    const API_VERSION = 5;
6
+    const API_VERSION = 2;
7
 
8
-    const ERROR_UNAUTHORIZED        = 401;
9
-    const ERROR_NOT_FOUND           = 404;
10
-    const ERROR_PRECONDITION_FAILED = 412;
11
-    const ERROR_CODE                = 500;
12
-    const ERROR_INVALID             = 501;
13
-    const ERROR_NOT_IMPLEMENTED     = 501;
14
+    const ERROR_CODE    = 500;
15
+    const ERROR_INVALID = 501;
16
 
17
     const OUTPUT_JSON = 'application/json';
18
     const OUTPUT_HTML = 'text/html';
19
 
20
-    public $env = array(
21
+    public $config = array(
22
         'date_format' => 'Y-m-d H:i',
23
         'language'    => 'en_US',
24
-        'timezone'    => 'UTC',
25
     );
26
 
27
     protected $app_name = 'Kolab File API';
28
     protected $drivers  = array();
29
-    protected $icache   = array();
30
     protected $backend;
31
 
32
     /**
33
@@ -72,7 +66,7 @@
34
         $this->backend = $this->load_driver_object($driver);
35
 
36
         // configure api
37
-        $this->backend->configure($this->env);
38
+        $this->backend->configure($this->config);
39
 
40
         return $this->backend;
41
     }
42
@@ -80,44 +74,22 @@
43
     /**
44
      * Return supported/enabled external storage instances
45
      *
46
-     * @param bool  $as_objects     Return drivers as objects not config data
47
-     * @param array &$admin_drivers List of admin-configured drivers
48
+     * @param bool $as_objects Return drivers as objects not config data
49
      *
50
      * @return array List of storage drivers
51
      */
52
-    public function get_drivers($as_objects = false, &$admin_drivers = null)
53
+    public function get_drivers($as_objects = false)
54
     {
55
         $rcube   = rcube::get_instance();
56
-        $backend = $this->get_backend();
57
         $enabled = $rcube->config->get('fileapi_drivers');
58
         $preconf = $rcube->config->get('fileapi_sources');
59
         $result  = array();
60
         $all     = array();
61
-        $iRony   = defined('KOLAB_DAV_ROOT');
62
-
63
-        // Disable webdav sources/drivers in iRony that point to the
64
-        // same host to prevent infinite recursion
65
-        $is_valid_source = function($source) {
66
-            if ($source['driver'] == 'webdav') {
67
-                $self_url = parse_url($_SERVER['SCRIPT_URI']);
68
-                $item_url = parse_url($source['baseuri'] ?: $source['host']);
69
-                $hosts    = array($self_url['host'], $_SERVER['SERVER_NAME'], $_SERVER['SERVER_ADDR']);
70
-
71
-                if (in_array($item_url['host'], $hosts)) {
72
-                    return false;
73
-                }
74
-            }
75
-
76
-            return true;
77
-        };
78
 
79
         if (!empty($enabled)) {
80
+            $backend = $this->get_backend();
81
             $drivers = $backend->driver_list();
82
 
83
-            if ($iRony) {
84
-                $drivers = array_filter($drivers, $is_valid_source);
85
-            }
86
-
87
             foreach ($drivers as $item) {
88
                 $all[] = $item['title'];
89
 
90
@@ -127,21 +99,13 @@
91
             }
92
         }
93
 
94
-        $admin_drivers = array();
95
-
96
-        if (!empty($preconf)) {
97
-            if ($iRony) {
98
-                $preconf = array_filter($preconf, $is_valid_source);
99
-            }
100
-
101
-            foreach ($preconf as $title => $item) {
102
+        if (empty($result) && !empty($preconf)) {
103
+            foreach ((array) $preconf as $title => $item) {
104
                 if (!in_array($title, $all)) {
105
                     $item['title'] = $title;
106
                     $item['admin'] = true;
107
 
108
                     $result[] = $as_objects ? $this->get_driver_object($item) : $item;
109
-
110
-                    $admin_drivers[] = $title;
111
                 }
112
             }
113
         }
114
@@ -170,11 +134,6 @@
115
         }
116
 
117
         if (empty($selected)) {
118
-            $rcube = rcube::get_instance();
119
-            if ($rcube->config->get('fileapi_backend_storage_disabled')) {
120
-                throw new Exception("Failed to find a driver for specified folder/file.", self::ERROR_NOT_FOUND);
121
-            }
122
-
123
             return array($this->get_backend(), $path);
124
         }
125
 
126
@@ -208,7 +167,7 @@
127
             }
128
 
129
             // configure api
130
-            $driver->configure(array_merge($config, $this->env), $key);
131
+            $driver->configure(array_merge($config, $this->config), $key);
132
         }
133
 
134
         return $this->drivers[$key];
135
@@ -233,15 +192,13 @@
136
     /**
137
      * Returns storage(s) capabilities
138
      *
139
-     * @param bool $full Return all drivers' capabilities
140
-     *
141
      * @return array Capabilities
142
      */
143
-    public function capabilities($full = true)
144
+    public function capabilities()
145
     {
146
         $rcube   = rcube::get_instance();
147
         $backend = $this->get_backend();
148
-        $caps    = array('VERSION' => self::API_VERSION);
149
+        $caps    = array();
150
 
151
         // check support for upload progress
152
         if (($progress_sec = $rcube->config->get('upload_progress'))
153
@@ -259,34 +216,6 @@
154
             }
155
         }
156
 
157
-        // Manticore support
158
-        if ($rcube->config->get('fileapi_manticore')) {
159
-            $caps['MANTICORE'] = true;
160
-        }
161
-
162
-        // WOPI support
163
-        if ($rcube->config->get('fileapi_wopi_office')) {
164
-            $caps['WOPI'] = true;
165
-        }
166
-
167
-        if ($rcube->config->get('fileapi_backend_storage_disabled')) {
168
-            $caps['NOROOT'] = true;
169
-        }
170
-
171
-        if (!$full) {
172
-            return $caps;
173
-        }
174
-
175
-        if ($caps['MANTICORE']) {
176
-            $manticore = new file_manticore($this);
177
-            $caps['MANTICORE_EDITABLE'] = $manticore->supported_filetypes(true);
178
-        }
179
-
180
-        if ($caps['WOPI']) {
181
-            $wopi = new file_wopi($this);
182
-            $caps['WOPI_EDITABLE'] = $wopi->supported_filetypes(true);
183
-        }
184
-
185
         // get capabilities of other drivers
186
         $drivers = $this->get_drivers(true);
187
 
188
@@ -296,7 +225,7 @@
189
                 foreach ($driver->capabilities() as $name => $value) {
190
                     // skip disabled capabilities
191
                     if ($value !== false) {
192
-                        $caps['MOUNTPOINTS'][$title][$name] = $value;
193
+                        $caps['roots'][$title][$name] = $value;
194
                     }
195
                 }
196
             }
197
@@ -306,48 +235,14 @@
198
     }
199
 
200
     /**
201
-     * Get user name from user identifier (email address) using LDAP lookup
202
-     *
203
-     * @param string $email User identifier
204
-     *
205
-     * @return string User name
206
-     */
207
-    public function resolve_user($email)
208
-    {
209
-        $key = "user:$email";
210
-
211
-        // make sure Kolab backend is initialized so kolab_storage can be found
212
-        $this->get_backend();
213
-
214
-        // @todo: Move this into drivers
215
-        if ($this->icache[$key] === null
216
-            && class_exists('kolab_storage')
217
-            && ($ldap = kolab_storage::ldap())
218
-        ) {
219
-            $user = $ldap->get_user_record($email, $_SESSION['imap_host']);
220
-
221
-            $this->icache[$key] = $user ?: false;
222
-        }
223
-
224
-        if ($this->icache[$key]) {
225
-            return $this->icache[$key]['displayname'] ?: $this->icache[$key]['name'];
226
-        }
227
-    }
228
-
229
-    /**
230
      * Return mimetypes list supported by built-in viewers
231
      *
232
      * @return array List of mimetypes
233
      */
234
     protected function supported_mimetypes()
235
     {
236
-        $rcube       = rcube::get_instance();
237
-        $mimetypes   = array();
238
-        $mimetypes_c = array();
239
-        $dir         = __DIR__ . '/viewers';
240
-
241
-        // make sure Kolab backend is initialized so kolab_auth can modify config
242
-        $backend = $this->get_backend();
243
+        $mimetypes = array();
244
+        $dir       = __DIR__ . '/viewers';
245
 
246
         if ($handle = opendir($dir)) {
247
             while (false !== ($file = readdir($handle))) {
248
@@ -356,38 +251,13 @@
249
                     $class  = 'file_viewer_' . $matches[1];
250
                     $viewer = new $class($this);
251
 
252
-                    if ($supported = $viewer->supported_mimetypes()) {
253
-                        $mimetypes = array_merge($mimetypes, $supported);
254
-                    }
255
+                    $mimetypes = array_merge($mimetypes, $viewer->supported_mimetypes());
256
                 }
257
             }
258
             closedir($handle);
259
         }
260
 
261
-        // Here we return mimetypes supported for editing and creation of files
262
-        // @TODO: maybe move this to viewers
263
-        if ($rcube->config->get('fileapi_wopi_office')) {
264
-            $mimetypes_c['application/vnd.oasis.opendocument.text']         = array('ext' => 'odt');
265
-            $mimetypes_c['application/vnd.oasis.opendocument.presentation'] = array('ext' => 'odp');
266
-            $mimetypes_c['application/vnd.oasis.opendocument.spreadsheet']  = array('ext' => 'ods');
267
-        }
268
-        else if ($rcube->config->get('fileapi_manticore')) {
269
-            $mimetypes_c['application/vnd.oasis.opendocument.text'] = array('ext' => 'odt');
270
-        }
271
-
272
-        $mimetypes_c['text/plain'] = array('ext' => 'txt');
273
-        $mimetypes_c['text/html']  = array('ext' => 'html');
274
-
275
-        foreach (array_keys($mimetypes_c) as $type) {
276
-            list ($app, $label) = explode('/', $type);
277
-            $label = preg_replace('/[^a-z]/', '', $label);
278
-            $mimetypes_c[$type]['label'] = $this->translate('type.' . $label);
279
-        }
280
-
281
-        return array(
282
-            'view' => $mimetypes,
283
-            'edit' => $mimetypes_c,
284
-        );
285
+        return $mimetypes;
286
     }
287
 
288
     /**
289
iRony-0.4.3.tar.gz/lib/FileAPI/file_api_lib.php -> iRony-0.4.4.tar.gz/lib/FileAPI/file_api_lib.php Changed
110
 
1
@@ -36,12 +36,12 @@
2
 
3
         switch ($name) {
4
             case 'configure':
5
-                foreach (array_keys($this->env) as $name) {
6
+                foreach (array_keys($this->config) as $name) {
7
                     if (isset($arguments[0][$name])) {
8
-                        $this->env[$name] = $arguments[0][$name];
9
+                        $this->config[$name] = $arguments[0][$name];
10
                     }
11
                 }
12
-                return $this->env;
13
+                return $this->config;
14
 
15
             case 'mimetypes':
16
                 return $this->supported_mimetypes();
17
@@ -84,29 +84,17 @@
18
                 return;
19
 
20
             case 'folder_list':
21
-                $args = array(
22
-                    'folder' => $arguments[0],
23
-                    'level'  => $arguments[1],
24
-                );
25
+                // no arguments
26
+                $args = array();
27
                 break;
28
 
29
             case 'folder_create':
30
-            case 'folder_subscribe':
31
-            case 'folder_unsubscribe':
32
             case 'folder_delete':
33
                 $args = array(
34
                     'folder' => $arguments[0],
35
                 );
36
                 break;
37
 
38
-            case 'folder_info':
39
-                $args = array(
40
-                    'folder'   => $arguments[0],
41
-                    'rights'   => 1,
42
-                    'sessions' => 1,
43
-                );
44
-                break;
45
-
46
             case 'folder_move':
47
                 $args = array(
48
                     'folder' => $arguments[0],
49
@@ -131,7 +119,6 @@
50
                 throw new Exception("Invalid method name", \file_storage::ERROR_UNSUPPORTED);
51
         }
52
 
53
-        require_once __DIR__ . "/api/common.php";
54
         require_once __DIR__ . "/api/$name.php";
55
 
56
         $class   = "file_api_$name";
57
@@ -147,3 +134,52 @@
58
     {
59
     }
60
 }
61
+
62
+
63
+/**
64
+ * Common handler class, from which action handler classes inherit
65
+ */
66
+class file_api_common
67
+{
68
+    protected $api;
69
+    protected $rc;
70
+    protected $args;
71
+
72
+
73
+    public function __construct($api, $args)
74
+    {
75
+        $this->rc   = rcube::get_instance();
76
+        $this->api  = $api;
77
+        $this->args = $args;
78
+    }
79
+
80
+    /**
81
+     * Request handler
82
+     */
83
+    public function handle()
84
+    {
85
+        // disable script execution time limit, so we can handle big files
86
+        @set_time_limit(0);
87
+    }
88
+
89
+    /**
90
+     * Parse driver metadata information
91
+     */
92
+    protected function parse_metadata($metadata, $default = false)
93
+    {
94
+        if ($default) {
95
+            unset($metadata['form']);
96
+            $metadata['name'] .= ' (' . $this->api->translate('localstorage') . ')';
97
+        }
98
+
99
+        // localize form labels
100
+        foreach ($metadata['form'] as $key => $val) {
101
+            $label = $this->api->translate('form.' . $val);
102
+            if (strpos($label, 'form.') !== 0) {
103
+                $metadata['form'][$key] = $label;
104
+            }
105
+        }
106
+
107
+        return $metadata;
108
+    }
109
+}
110
iRony-0.4.3.tar.gz/lib/FileAPI/file_locale.php -> iRony-0.4.4.tar.gz/lib/FileAPI/file_locale.php Changed
32
 
1
@@ -26,7 +26,7 @@
2
 class file_locale
3
 {
4
     protected static $translation = array();
5
-    protected $language;
6
+
7
 
8
     /**
9
      * Localization initialization.
10
@@ -40,8 +40,6 @@
11
             $language = 'en_US';
12
         }
13
 
14
-        $this->language = $language;
15
-
16
         @include __DIR__ . "/locale/en_US.php";
17
 
18
         if ($language != 'en_US') {
19
@@ -73,11 +71,8 @@
20
         if (!empty($_SESSION['user']) && !empty($_SESSION['user']['language'])) {
21
             array_unshift($langs, $_SESSION['user']['language']);
22
         }
23
-        if (!empty($_SESSION['env']) && !empty($_SESSION['env']['language'])) {
24
-            array_unshift($langs, $_SESSION['env']['language']);
25
-        }
26
 
27
-        foreach (array_unique($langs) as $lang) {
28
+        while ($lang = array_shift($langs)) {
29
             $lang = explode(';', $lang);
30
             $lang = $lang[0];
31
             $lang = str_replace('-', '_', $lang);
32
iRony-0.4.3.tar.gz/lib/FileAPI/file_locks.php -> iRony-0.4.4.tar.gz/lib/FileAPI/file_locks.php Changed
12
 
1
@@ -28,8 +28,8 @@
2
  * It stores all its data in a sql database. Derived from SabreDAV's
3
  * PDO Lock manager.
4
  */
5
-class file_locks
6
-{
7
+class file_locks {
8
+
9
     const SHARED    = 1;
10
     const EXCLUSIVE = 2;
11
     const INFINITE  = -1;
12
iRony-0.4.3.tar.gz/lib/FileAPI/file_storage.php -> iRony-0.4.4.tar.gz/lib/FileAPI/file_storage.php Changed
137
 
1
@@ -31,17 +31,13 @@
2
     const CAPS_PROGRESS_TIME = 'PROGRESS_TIME';
3
     const CAPS_QUOTA         = 'QUOTA';
4
     const CAPS_LOCKS         = 'LOCKS';
5
-    const CAPS_SUBSCRIPTIONS = 'SUBSCRIPTIONS';
6
-    const CAPS_FAST_FOLDER_LIST = 'FAST_FOLDER_LIST';
7
 
8
     // config
9
     const SEPARATOR = '/';
10
 
11
     // error codes
12
-    const ERROR_LOCKED      = 423;
13
     const ERROR             = 500;
14
-    const ERROR_UNAVAILABLE = 503;
15
-    const ERROR_FORBIDDEN   = 530;
16
+    const ERROR_LOCKED      = 423;
17
     const ERROR_FILE_EXISTS = 550;
18
     const ERROR_UNSUPPORTED = 570;
19
     const ERROR_NOAUTH      = 580;
20
@@ -51,23 +47,6 @@
21
     const LOCK_EXCLUSIVE = 'exclusive';
22
     const LOCK_INFINITE  = 'infinite';
23
 
24
-    // list filters
25
-    const FILTER_UNSUBSCRIBED = 1;
26
-    const FILTER_WRITABLE     = 2;
27
-
28
-    // folder permissions
29
-    const ACL_READ = 1;
30
-    const ACL_WRITE = 2;
31
-
32
-    // sharing interface modes
33
-    const SHARING_MODE_FORM   = 1;
34
-    const SHARING_MODE_RIGHTS = 2;
35
-    const SHARING_MODE_UPDATE = 3;
36
-
37
-    // search modes
38
-    const SEARCH_USER   = 1;
39
-    const SEARCH_GROUP  = 2;
40
-
41
 
42
     /**
43
      * Authenticates a user
44
@@ -271,41 +250,12 @@
45
     public function folder_move($folder_name, $new_name);
46
 
47
     /**
48
-     * Subscribe a folder.
49
-     *
50
-     * @param string $folder_name Name of a folder with full path
51
-     *
52
-     * @throws Exception
53
-     */
54
-    public function folder_subscribe($folder_name);
55
-
56
-    /**
57
-     * Unsubscribe a folder.
58
-     *
59
-     * @param string $folder_name Name of a folder with full path
60
-     *
61
-     * @throws Exception
62
-     */
63
-    public function folder_unsubscribe($folder_name);
64
-
65
-    /**
66
      * Returns list of folders.
67
      *
68
-     * @param array $params List parameters ('type', 'search', 'extended', 'permissions')
69
-     *
70
      * @return array List of folders
71
      * @throws Exception
72
      */
73
-    public function folder_list($params = array());
74
-
75
-    /**
76
-     * Check folder rights.
77
-     *
78
-     * @param string $folder_name Name of a folder with full path
79
-     *
80
-     * @return int Folder rights (sum of file_storage::ACL_*)
81
-     */
82
-    public function folder_rights($folder_name);
83
+    public function folder_list();
84
 
85
     /**
86
      * Returns a list of locks
87
@@ -358,49 +308,4 @@
88
      * @throws Exception
89
      */
90
     public function quota($folder);
91
-
92
-    /**
93
-     * Sharing interface
94
-     *
95
-     * @param string $folder_name Name of a folder with full path
96
-     * @param int    $mode        Sharing action mode
97
-     * @param array  $args        POST/GET parameters
98
-     *
99
-     * @return mixed Sharing response
100
-     * @throws Exception
101
-     */
102
-    public function sharing($folder, $mode, $args = array());
103
-
104
-    /**
105
-     * User/group search (autocompletion)
106
-     *
107
-     * @param string $search Search string
108
-     * @param int    $mode   Search mode
109
-     *
110
-     * @return array Users/Groups list
111
-     * @throws Exception
112
-     */
113
-    public function autocomplete($search, $mode);
114
-
115
-    /**
116
-     * Convert file/folder path into a global URI.
117
-     * Note: We're using self::SEPARATOR as a hierarchy delimiter
118
-     *
119
-     * @param string $path File/folder path
120
-     *
121
-     * @return string URI
122
-     * @throws Exception
123
-     */
124
-    public function path2uri($path);
125
-
126
-    /**
127
-     * Convert global URI into file/folder path.
128
-     * Note: We're using self::SEPARATOR as a hierarchy delimiter
129
-     *
130
-     * @param string $uri URI
131
-     *
132
-     * @return string File/folder path
133
-     * @throws Exception
134
-     */
135
-    public function uri2path($uri);
136
 }
137
iRony-0.4.3.tar.gz/lib/FileAPI/file_ui.php -> iRony-0.4.4.tar.gz/lib/FileAPI/file_ui.php Changed
57
 
1
@@ -40,11 +40,11 @@
2
      */
3
     protected $config;
4
 
5
-    protected $ajax_only    = false;
6
-    protected $page_title   = 'Kolab File API';
7
-    protected $menu         = array();
8
-    protected $cache        = array();
9
-    protected $devel_mode   = false;
10
+    protected $ajax_only = false;
11
+    protected $page_title = 'Kolab File API';
12
+    protected $menu = array();
13
+    protected $cache = array();
14
+    protected $devel_mode = false;
15
     protected $object_types = array();
16
 
17
     const API_VERSION = 2;
18
@@ -58,7 +58,7 @@
19
     public function __construct($output = null)
20
     {
21
         $rcube = rcube::get_instance();
22
-        register_shutdown_function(array($this, 'shutdown'));
23
+        $rcube->add_shutdown_function(array($this, 'shutdown'));
24
 
25
         $this->config_init();
26
 
27
@@ -355,21 +355,14 @@
28
     public function shutdown()
29
     {
30
         // write performance stats to logs/console
31
-        if ($this->config->get('devel_mode') || $this->config->get('performance_stats')) {
32
-            // we have to disable per_user_logging to make sure stats end up in the main console log
33
-            $this->config->set('per_user_logging', false);
34
-
35
-            // make sure logged numbers use unified format
36
-            setlocale(LC_NUMERIC, 'en_US.utf8', 'en_US.UTF-8', 'en_US', 'C');
37
-
38
-            if (function_exists('memory_get_usage')) {
39
-                $mem = round(memory_get_usage() / 1024 /1024, 1);
40
-            }
41
-            if (function_exists('memory_get_peak_usage')) {
42
-                $mem .= '/'. round(memory_get_peak_usage() / 1024 / 1024, 1);
43
-            }
44
-
45
-            $log = 'ui:' . $this->get_task() . ($this->action ? '/' . $this->action : '') . " [$mem]";
46
+        if ($this->devel_mode) {
47
+            if (function_exists('memory_get_peak_usage'))
48
+                $mem = memory_get_peak_usage();
49
+            else if (function_exists('memory_get_usage'))
50
+                $mem = memory_get_usage();
51
+
52
+            $log = 'ui:' . $this->get_task() . ($this->action ? '/' . $this->action : '');
53
+            $log .= ($mem ? sprintf(' [%.1f MB]', $mem/1024/1024) : '');
54
 
55
             if (defined('FILE_API_START')) {
56
                 rcube::print_timer(FILE_API_START, $log);
57
iRony-0.4.3.tar.gz/lib/FileAPI/file_ui_api.php -> iRony-0.4.4.tar.gz/lib/FileAPI/file_ui_api.php Changed
13
 
1
@@ -280,10 +280,7 @@
2
             $err_str  = 'Unable to decode response';
3
         }
4
 
5
-        if (!$err_code && array_key_exists('result', (array) $body)) {
6
-            $body = $body['result'];
7
-        }
8
-
9
         return new file_ui_api_result($body, $err_code, $err_str);
10
     }
11
+
12
 }
13
iRony-0.4.3.tar.gz/lib/FileAPI/file_ui_api_result.php -> iRony-0.4.4.tar.gz/lib/FileAPI/file_ui_api_result.php Changed
17
 
1
@@ -45,12 +45,12 @@
2
      */
3
     public function __construct($data = array(), $error_code = null, $error_str = null)
4
     {
5
-        if (is_array($data)) {
6
-            $this->data = $data;
7
+        if (is_array($data) && isset($data['result'])) {
8
+            $this->data = $data['result'];
9
         }
10
 
11
         $this->error_code = $error_code;
12
-        $this->error_str  = $error_str;
13
+        $this->error_str = $error_str;
14
     }
15
 
16
     /**
17
iRony-0.4.3.tar.gz/lib/FileAPI/file_utils.php -> iRony-0.4.4.tar.gz/lib/FileAPI/file_utils.php Changed
90
 
1
@@ -170,79 +170,20 @@
2
     }
3
 
4
     /**
5
-     * Callback for uasort() that implements correct
6
-     * locale-aware case-sensitive sorting
7
-     */
8
-    public static function sort_folder_comparator($p1, $p2)
9
-    {
10
-        $ext   = is_array($p1); // folder can be a string or an array with 'folder' key
11
-        $path1 = explode(file_storage::SEPARATOR, $ext ? $p1['folder'] : $p1);
12
-        $path2 = explode(file_storage::SEPARATOR, $ext ? $p2['folder'] : $p2);
13
-
14
-        foreach ($path1 as $idx => $folder1) {
15
-            $folder2 = $path2[$idx];
16
-
17
-            if ($folder1 === $folder2) {
18
-                continue;
19
-            }
20
-
21
-            return strcoll($folder1, $folder2);
22
-        }
23
-
24
-        return 0;
25
-    }
26
-
27
-    /**
28
-     * Encode folder path for use in an URI
29
-     *
30
-     * @param string $path Folder path
31
-     *
32
-     * @return string Encoded path
33
-     */
34
-    public static function encode_path($path)
35
-    {
36
-        $items = explode(file_storage::SEPARATOR, $path);
37
-        $items = array_map('rawurlencode', $items);
38
-
39
-        return implode(file_storage::SEPARATOR, $items);
40
-    }
41
-
42
-    /**
43
-     * Decode an URI into folder path
44
+     * Returns script URI
45
      *
46
-     * @param string $path Encoded folder path
47
-     *
48
-     * @return string Decoded path
49
+     * @return string Script URI
50
      */
51
-    public static function decode_path($path)
52
+    static function script_uri()
53
     {
54
-        $items = explode(file_storage::SEPARATOR, $path);
55
-        $items = array_map('rawurldecode', $items);
56
-
57
-        return implode(file_storage::SEPARATOR, $items);
58
-    }
59
-
60
-    /**
61
-     *
62
-     * @return string Date time string in specified format and timezone
63
-     */
64
-    public static function date_format($datetime, $format = 'Y-m-d H:i', $timezone = null)
65
-    {
66
-        if (!$datetime instanceof DateTime) {
67
-            return '';
68
+        if (!empty($_SERVER['SCRIPT_URI'])) {
69
+            return $_SERVER['SCRIPT_URI'];
70
         }
71
 
72
-        if ($timezone && $timezone != $datetime->getTimezone()) {
73
-            try {
74
-                $dt = clone $datetime;
75
-                $dt->setTimezone(new DateTimeZone($timezone));
76
-                $datetime = $dt;
77
-            }
78
-            catch (Exception $e) {
79
-                // ignore, return original timezone
80
-            }
81
-        }
82
+        $uri = $_SERVER['SERVER_PORT'] == 443 ? 'https://' : 'http://';
83
+        $uri .= $_SERVER['HTTP_HOST'];
84
+        $uri .= preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI']);
85
 
86
-        return $datetime->format($format);
87
+        return $uri;
88
     }
89
 }
90
iRony-0.4.3.tar.gz/lib/FileAPI/file_viewer.php -> iRony-0.4.4.tar.gz/lib/FileAPI/file_viewer.php Changed
15
 
1
@@ -66,10 +66,10 @@
2
     /**
3
      * Print output and exit
4
      *
5
-     * @param string $file      File name
6
-     * @param array  $file_info File metadata (e.g. type)
7
+     * @param string $file     File name
8
+     * @param string $mimetype File type
9
      */
10
-    public function output($file, $file_info = array())
11
+    public function output($file, $mimetype = null)
12
     {
13
     }
14
 
15
iRony-0.4.3.tar.gz/lib/FileAPI/init.php -> iRony-0.4.4.tar.gz/lib/FileAPI/init.php Changed
21
 
1
@@ -30,16 +30,11 @@
2
 define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/drivers/kolab/plugins');
3
 
4
 // Define include path
5
-$include_path  = RCUBE_INSTALL_PATH . 'lib' . PATH_SEPARATOR;
6
-$include_path .= RCUBE_INSTALL_PATH . 'lib/ext' . PATH_SEPARATOR;
7
-$include_path .= RCUBE_INSTALL_PATH . 'lib/client' . PATH_SEPARATOR;
8
+$include_path  = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
9
+$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
10
+$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
11
 $include_path .= ini_get('include_path');
12
 set_include_path($include_path);
13
 
14
-// include composer autoloader (if available)
15
-if (@file_exists(RCUBE_INSTALL_PATH . 'vendor/autoload.php')) {
16
-    require RCUBE_INSTALL_PATH . 'vendor/autoload.php';
17
-}
18
-
19
 // include global functions from Roundcube Framework
20
 require_once 'Roundcube/bootstrap.php';
21
iRony-0.4.3.tar.gz/lib/FileAPI/locale/en_US.php -> iRony-0.4.4.tar.gz/lib/FileAPI/locale/en_US.php Changed
48
 
1
@@ -41,14 +41,12 @@
2
 $LANG['folder.driverwithpassdesc'] = 'Stored passwords will be encrypted. Enable this if you do not want to be asked for the password on every login or you want this storage to be available via WebDAV.';
3
 $LANG['folder.name'] = 'Name:';
4
 $LANG['folder.authenticate'] = 'Logon to $title';
5
-$LANG['folder.parent'] = 'Subfolder of:';
6
 
7
 $LANG['form.submit'] = 'Submit';
8
 $LANG['form.cancel'] = 'Cancel';
9
 $LANG['form.hostname'] = 'Hostname:';
10
 $LANG['form.username'] = 'Username:';
11
 $LANG['form.password'] = 'Password:';
12
-$LANG['form.baseuri'] = 'Base URI:';
13
 
14
 $LANG['login.username'] = 'Username';
15
 $LANG['login.password'] = 'Password';
16
@@ -75,31 +73,11 @@
17
 $LANG['search.in_all_folders'] = 'in all folders';
18
 $LANG['search.in_current_folder'] = 'in current folder';
19
 
20
-$LANG['share.permissions'] = 'Permissions';
21
-$LANG['share.readonly'] = 'read-only';
22
-$LANG['share.readwrite'] = 'read-write';
23
-$LANG['share.admin'] = 'administrator';
24
-$LANG['share.usergroup'] = 'User or Group';
25
-$LANG['share.permission'] = 'Permission';
26
-$LANG['share.upload-link'] = 'Upload Link';
27
-$LANG['share.download-link'] = 'Download Link';
28
-$LANG['share.password'] = 'Password';
29
-$LANG['share.expire'] = 'Expiration';
30
-$LANG['share.expiredays'] = 'Expiration time in days';
31
-$LANG['share.generate'] = 'Generate';
32
-$LANG['share.link'] = 'Link';
33
-
34
 $LANG['size.B'] = 'B';
35
 $LANG['size.KB'] = 'KB';
36
 $LANG['size.MB'] = 'MB';
37
 $LANG['size.GB'] = 'GB';
38
 
39
-$LANG['type.vndoasisopendocumenttext'] = 'OpenDocument Document (.odt)';
40
-$LANG['type.vndoasisopendocumentpresentation'] = 'OpenDocument Presentation (.odp)';
41
-$LANG['type.vndoasisopendocumentspreadsheet'] = 'OpenDocument Spreadsheet (.ods)';
42
-$LANG['type.plain'] = 'Plain Text Document (.txt)';
43
-$LANG['type.html'] = 'HTML Document (.html)';
44
-
45
 $LANG['upload.size'] = 'Size:';
46
 $LANG['upload.size.error'] = 'Maximum upload size ($size) exceeded!';
47
 $LANG['upload.progress'] = 'Progress:';
48
iRony-0.4.3.tar.gz/lib/FileAPI/viewers/image.php -> iRony-0.4.4.tar.gz/lib/FileAPI/viewers/image.php Changed
31
 
1
@@ -60,7 +60,9 @@
2
      */
3
     public function href($file, $mimetype = null)
4
     {
5
-        $href = $this->api->file_url($file);
6
+        $href = file_utils::script_uri() . '?method=file_get'
7
+            . '&file=' . urlencode($file)
8
+            . '&token=' . urlencode(session_id());
9
 
10
         // we redirect to self only images with types unsupported
11
         // by browser
12
@@ -74,14 +76,14 @@
13
     /**
14
      * Print output and exit
15
      *
16
-     * @param string $file      File name
17
-     * @param array  $file_info File metadata (e.g. type)
18
+     * @param string $file     File name
19
+     * @param string $mimetype File type
20
      */
21
-    public function output($file, $file_info = array())
22
+    public function output($file, $mimetype = null)
23
     {
24
 /*
25
         // conversion not needed
26
-        if (preg_match('/^image/p?jpe?g$/i', $file_info['type'])) {
27
+        if (preg_match('/^image/p?jpe?g$/i', $mimetype)) {
28
             $this->api->api->file_get($file);
29
             return;
30
         }
31
iRony-0.4.3.tar.gz/lib/FileAPI/viewers/media.php -> iRony-0.4.4.tar.gz/lib/FileAPI/viewers/media.php Changed
41
 
1
@@ -67,7 +67,7 @@
2
      */
3
     public function frame($file, $mimetype = null)
4
     {
5
-        $path     = $this->api->api_url();
6
+        $path     = file_utils::script_uri();
7
         $file_uri = htmlentities($this->api->file_url($file));
8
         $mimetype = htmlentities($mimetype);
9
         $source   = "<source src=\"$file_uri\" type=\"$mimetype\"></source>";
10
@@ -80,21 +80,26 @@
11
         }
12
 
13
         return <<<EOT
14
-    <link rel="stylesheet" type="text/css" href="$path/viewers/media/mediaelementplayer.css" />
15
-    <script type="text/javascript" src="$path/viewers/media/mediaelement-and-player.js"></script>
16
+    <link rel="stylesheet" type="text/css" href="{$path}viewers/media/mediaelementplayer.css" />
17
+    <script type="text/javascript" src="{$path}viewers/media/mediaelement-and-player.js"></script>
18
     <$tag id="media-player" controls preload="auto">$source</$tag>
19
     <style>
20
       .mejs-container { text-align: center; }
21
     </style>
22
     <script>
23
-      var height = '100%';
24
-        width = '100%';
25
+      var content_frame = $('#media-player').parent(),
26
+        height = content_frame.height(),
27
+        width = content_frame.width(),
28
         player = new MediaElementPlayer('#media-player', {
29
           videoHeight: height, audioHeight: height, videoWidth: width, audioWidth: width
30
         });
31
 
32
       player.pause();
33
       player.play();
34
+      // add player resize handler
35
+      $(window).resize(function() {
36
+        player.setPlayerSize(content_frame.width(), content_frame.height());
37
+      });
38
     </script>
39
 EOT;
40
     }
41
iRony-0.4.3.tar.gz/lib/FileAPI/viewers/odf.php -> iRony-0.4.4.tar.gz/lib/FileAPI/viewers/odf.php Changed
43
 
1
@@ -90,6 +90,17 @@
2
     }
3
 
4
     /**
5
+     * Return output of file content area
6
+     *
7
+     * @param string $file     File name
8
+     * @param string $mimetype File type
9
+     */
10
+    public function frame($file, $mimetype = null)
11
+    {
12
+        // we use iframe method, see output()
13
+    }
14
+
15
+    /**
16
      * Return file viewer URL
17
      *
18
      * @param string $file     File name
19
@@ -97,16 +108,19 @@
20
      */
21
     public function href($file, $mimetype = null)
22
     {
23
-        return $this->api->file_url($file) . '&viewer=odf';
24
+        return file_utils::script_uri() . '?method=file_get'
25
+            . '&viewer=odf'
26
+            . '&file=' . urlencode($file)
27
+            . '&token=' . urlencode(session_id());
28
     }
29
 
30
     /**
31
      * Print output and exit
32
      *
33
-     * @param string $file      File name
34
-     * @param array  $file_info File metadata (e.g. type)
35
+     * @param string $file     File name
36
+     * @param string $mimetype File type
37
      */
38
-    public function output($file, $file_info = array())
39
+    public function output($file, $mimetype = null)
40
     {
41
         $file_uri = $this->api->file_url($file);
42
 
43
iRony-0.4.3.tar.gz/lib/FileAPI/viewers/pdf.php -> iRony-0.4.4.tar.gz/lib/FileAPI/viewers/pdf.php Changed
10
 
1
@@ -67,7 +67,7 @@
2
      */
3
     public function href($file, $mimetype = null)
4
     {
5
-        return $this->api->api_url() . '/viewers/pdf/viewer.html'
6
+        return file_utils::script_uri() . 'viewers/pdf/viewer.html'
7
             . '?file=' . urlencode($this->api->file_url($file));
8
     }
9
 }
10
iRony-0.4.3.tar.gz/lib/FileAPI/viewers/text.php -> iRony-0.4.4.tar.gz/lib/FileAPI/viewers/text.php Changed
18
 
1
@@ -123,12 +123,12 @@
2
     /**
3
      * Print output and exit
4
      *
5
-     * @param string $file      File name
6
-     * @param array  $file_info File metadata (e.g. type)
7
+     * @param string $file     File name
8
+     * @param string $mimetype File type
9
      */
10
-    public function output($file, $file_info = array())
11
+    public function output($file, $mimetype = null)
12
     {
13
-        $mode = $this->get_mode($file_info['type'], $file);
14
+        $mode = $this->get_mode($mimetype, $file);
15
         $href = addcslashes($this->api->file_url($file), "'");
16
 
17
         echo '<!DOCTYPE html>
18
iRony-0.4.3.tar.gz/lib/Kolab/CalDAV/Plugin.php -> iRony-0.4.4.tar.gz/lib/Kolab/CalDAV/Plugin.php Changed
16
 
1
@@ -239,11 +239,9 @@
2
      */
3
     function propFind(DAV\PropFind $propFind, DAV\INode $node)
4
     {
5
-        if ($node instanceof DAV\SimpleCollection) {
6
-            $propFind->handle('{' . self::NS_CALDAV . '}calendar-home-set', function() {
7
-                return new DAV\Property\Href($this->getCalendarHomeForPrincipal(HTTPBasic::$current_user) . '/');
8
-            });
9
-        }
10
+        $propFind->handle('{' . self::NS_CALDAV . '}calendar-home-set', function() {
11
+            return new DAV\Property\Href($this->getCalendarHomeForPrincipal(HTTPBasic::$current_user) . '/');
12
+        });
13
 
14
         parent::propFind($propFind, $node);
15
     }
16
iRony-0.4.3.tar.gz/lib/Kolab/CardDAV/ContactsBackend.php -> iRony-0.4.4.tar.gz/lib/Kolab/CardDAV/ContactsBackend.php Changed
22
 
1
@@ -47,6 +47,7 @@
2
     );
3
 
4
     public $ldap_directory;
5
+    public $ldap_resources;
6
 
7
     private $sources;
8
     private $folders;
9
@@ -363,6 +364,12 @@
10
                 $contact = $this->ldap_directory->getContactObject($uid);
11
             }
12
         }
13
+        // read card data from LDAP resources
14
+        else if ($addressBookId == LDAPResources::DIRECTORY_NAME) {
15
+            if (is_object($this->ldap_resources)) {
16
+                $contact = $this->ldap_resources->getContactObject($uid);
17
+            }
18
+        }
19
         else {
20
             $storage = $this->get_storage_folder($addressBookId);
21
             $contact = $storage->get_object($uid, '*');
22
iRony-0.4.3.tar.gz/lib/Kolab/CardDAV/LDAPDirectory.php -> iRony-0.4.4.tar.gz/lib/Kolab/CardDAV/LDAPDirectory.php Changed
33
 
1
@@ -42,14 +42,14 @@
2
 {
3
     const DIRECTORY_NAME = 'ldap-directory';
4
 
5
-    private $config;
6
-    private $ldap;
7
-    private $carddavBackend;
8
-    private $principalUri;
9
-    private $addressBookInfo = array();
10
-    private $cache;
11
-    private $query;
12
-    private $filter;
13
+    protected $config;
14
+    protected $ldap;
15
+    protected $carddavBackend;
16
+    protected $principalUri;
17
+    protected $addressBookInfo = array();
18
+    protected $cache;
19
+    protected $query;
20
+    protected $filter;
21
 
22
     /**
23
      * Default constructor
24
@@ -417,6 +417,8 @@
25
         }
26
         else if (!empty($contact['changed'])) {
27
             try {
28
+                //                                      2018   05     14     06     22     31    .0Z     2018-05-14T06:22:31
29
+                $contact['changed'] = preg_replace('/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.0Z$/', '$1-$2-$3T$4:$5:$6', $contact['changed']);
30
                 $contact['changed'] = new \DateTime($contact['changed']);
31
                 $contact['_timestamp'] = intval($contact['changed']->format('U'));
32
             }
33
iRony-0.4.3.tar.gz/lib/Kolab/CardDAV/Plugin.php -> iRony-0.4.4.tar.gz/lib/Kolab/CardDAV/Plugin.php Changed
38
 
1
@@ -68,16 +68,19 @@
2
      */
3
     public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node)
4
     {
5
-        // publish global ldap address book for this principal
6
-        if ($node instanceof DAVACL\IPrincipal && empty($this->directories) && \rcube::get_instance()->config->get('kolabdav_ldap_directory')) {
7
-            $this->directories[] = self::ADDRESSBOOK_ROOT . '/' . $node->getName() . '/' . LDAPDirectory::DIRECTORY_NAME;
8
+        // publish global ldap address book and resources list for this principal
9
+        if ($node instanceof DAVACL\IPrincipal && empty($this->directories)) {
10
+            if (\rcube::get_instance()->config->get('kolabdav_ldap_directory')) {
11
+                $this->directories[] = self::ADDRESSBOOK_ROOT . '/' . $node->getName() . '/' . LDAPDirectory::DIRECTORY_NAME;
12
+            }
13
+            if (\rcube::get_instance()->config->get('kolabdav_ldap_resources')) {
14
+                $this->directories[] = self::ADDRESSBOOK_ROOT . '/' . $node->getName() . '/' . LDAPResources::DIRECTORY_NAME;
15
+            }
16
         }
17
 
18
-        if ($node instanceof DAV\SimpleCollection) {
19
-            $propFind->handle('{' . self::NS_CARDDAV . '}addressbook-home-set', function() {
20
-                return new DAV\Property\Href($this->getAddressBookHomeForPrincipal(HTTPBasic::$current_user) . '/');
21
-            });
22
-        }
23
+        $propFind->handle('{' . self::NS_CARDDAV . '}addressbook-home-set', function() {
24
+            return new DAV\Property\Href($this->getAddressBookHomeForPrincipal(HTTPBasic::$current_user) . '/');
25
+        });
26
 
27
         parent::propFindEarly($propFind, $node);
28
     }
29
@@ -243,7 +246,7 @@
30
         }
31
 
32
         // query on LDAP node: pass along filter query
33
-        if ($node instanceof LDAPDirectory) {
34
+        if ($node instanceof LDAPDirectory || $node instanceof LDAPResources) {
35
             $query = new CardDAV\AddressBookQueryParser($dom);
36
             $query->parse();
37
 
38
iRony-0.4.3.tar.gz/lib/Kolab/CardDAV/UserAddressBooks.php -> iRony-0.4.4.tar.gz/lib/Kolab/CardDAV/UserAddressBooks.php Changed
52
 
1
@@ -37,6 +37,7 @@
2
 {
3
     // pseudo-singleton instance
4
     private $ldap_directory;
5
+    private $ldap_resources;
6
 
7
     /**
8
      * Returns a list of addressbooks
9
@@ -55,6 +56,10 @@
10
             $objs[] = $this->getLDAPDirectory();
11
         }
12
 
13
+        if (rcube::get_instance()->config->get('kolabdav_ldap_resources')) {
14
+            $objs[] = $this->getLDAPResources();
15
+        }
16
+
17
         return $objs;
18
     }
19
 
20
@@ -69,6 +74,9 @@
21
         if ($name == LDAPDirectory::DIRECTORY_NAME) {
22
             return $this->getLDAPDirectory();
23
         }
24
+        if ($name == LDAPResources::DIRECTORY_NAME) {
25
+            return $this->getLDAPResources();
26
+        }
27
 
28
         if ($addressbook = $this->carddavBackend->getAddressBookByName($name)) {
29
             $addressbook['principaluri'] = $this->principalUri;
30
@@ -93,6 +101,21 @@
31
         return $this->ldap_directory;
32
     }
33
 
34
+    /**
35
+     * Getter for the singleton instance of the LDAP resources
36
+     */
37
+    private function getLDAPResources()
38
+    {
39
+        if (!$this->ldap_resources) {
40
+            $rcube = rcube::get_instance();
41
+            $config = $rcube->config->get('kolabdav_ldap_resources');
42
+            $config['debug'] = $rcube->config->get('ldap_debug');
43
+            $this->ldap_resources = new LDAPResources($config, $this->principalUri, $this->carddavBackend);
44
+        }
45
+
46
+        return $this->ldap_resources;
47
+    }
48
+
49
 
50
     /**
51
      * Returns the list of properties
52
iRony-0.4.3.tar.gz/lib/Roundcube/bootstrap.php -> iRony-0.4.4.tar.gz/lib/Roundcube/bootstrap.php Changed
10
 
1
@@ -58,7 +58,7 @@
2
 }
3
 
4
 // framework constants
5
-define('RCUBE_VERSION', '1.4.2');
6
+define('RCUBE_VERSION', '1.4.9');
7
 define('RCUBE_CHARSET', 'UTF-8');
8
 define('RCUBE_TEMP_FILE_PREFIX', 'RCMTEMP');
9
 
10
iRony-0.4.3.tar.gz/lib/Roundcube/cache/memcached.php -> iRony-0.4.4.tar.gz/lib/Roundcube/cache/memcached.php Changed
10
 
1
@@ -107,7 +107,7 @@
2
                     if (!$port) $port = 11211;
3
                 }
4
                 else {
5
-                    $host = substr($host, 8);
6
+                    $host = substr($host, 7);
7
                     $port = 0;
8
                 }
9
 
10
iRony-0.4.3.tar.gz/lib/Roundcube/db/sqlsrv.php -> iRony-0.4.4.tar.gz/lib/Roundcube/db/sqlsrv.php Changed
51
 
1
@@ -27,6 +27,49 @@
2
  */
3
 class rcube_db_sqlsrv extends rcube_db_mssql
4
 {
5
+
6
+    /**
7
+     * Get last inserted record ID
8
+     *
9
+     * @param string $table Table name (to find the incremented sequence)
10
+     *
11
+     * @return string|false The ID or False on failure
12
+     */
13
+    public function insert_id($table = '')
14
+    {
15
+        if (!$this->db_connected || $this->db_mode == 'r') {
16
+            return false;
17
+        }
18
+
19
+        if ($table) {
20
+            // For some unknown reason the constant described in the driver docs
21
+            // might not exist, we'll fallback to PDO::ATTR_CLIENT_VERSION (#7564)
22
+            if (defined('PDO::ATTR_DRIVER_VERSION')) {
23
+                $driver_version = $this->dbh->getAttribute(PDO::ATTR_DRIVER_VERSION);
24
+            }
25
+            else if (defined('PDO::ATTR_CLIENT_VERSION')) {
26
+                $client_version = $this->dbh->getAttribute(PDO::ATTR_CLIENT_VERSION);
27
+                $driver_version = $client_version['ExtensionVer'];
28
+            }
29
+            else {
30
+                $driver_version = 5;
31
+            }
32
+
33
+            // Starting from version 5 of the driver lastInsertId() method expects
34
+            // a sequence name instead of a table name. We'll unset the argument
35
+            // to get the last insert sequence (#7564)
36
+            if (version_compare($driver_version, '5', '>=')) {
37
+                $table = null;
38
+            }
39
+            else {
40
+                // resolve table name
41
+                $table = $this->table_name($table);
42
+            }
43
+        }
44
+
45
+        return $this->dbh->lastInsertId($table);
46
+    }
47
+
48
     /**
49
      * Returns PDO DSN string from DSN array
50
      */
51
iRony-0.4.3.tar.gz/lib/Roundcube/rcube.php -> iRony-0.4.4.tar.gz/lib/Roundcube/rcube.php Changed
41
 
1
@@ -1124,6 +1124,19 @@
2
     }
3
 
4
     /**
5
+     * Quote a given string, remove new-line characters, use strict mode.
6
+     * Shortcut function for rcube_utils::rep_specialchars_output()
7
+     *
8
+     * @param string $str A string to quote
9
+     *
10
+     * @return string HTML-quoted string
11
+     */
12
+    public static function SQ($str)
13
+    {
14
+        return rcube_utils::rep_specialchars_output($str, 'html', 'strict', false);
15
+    }
16
+
17
+    /**
18
      * Construct shell command, execute it and return output as string.
19
      * Keywords {keyword} are replaced with arguments
20
      *
21
@@ -1253,7 +1266,7 @@
22
         // write message with file name when configured to log to STDOUT
23
         if ($log_driver == 'stdout') {
24
             $stdout = "php://stdout";
25
-            $line = "$name: $line";
26
+            $line = "$name: $line\n";
27
             return file_put_contents($stdout, $line, FILE_APPEND) !== false;
28
         }
29
 
30
@@ -1607,8 +1620,8 @@
31
      * @param string       $from        Sender address string
32
      * @param array|string $mailto      Either a comma-separated list of recipients (RFC822 compliant),
33
      *                                  or an array of recipients, each RFC822 valid
34
-     * @param array        &$error      SMTP error array (reference)
35
-     * @param string       &$body_file  Location of file with saved message body (reference),
36
+     * @param array|string &$error      SMTP error array or (deprecated) string
37
+     * @param string       &$body_file  Location of file with saved message body,
38
      *                                  used when delay_file_io is enabled
39
      * @param array        $options     SMTP options (e.g. DSN request)
40
      * @param bool         $disconnect  Close SMTP connection ASAP
41
iRony-0.4.3.tar.gz/lib/Roundcube/rcube_config.php -> iRony-0.4.4.tar.gz/lib/Roundcube/rcube_config.php Changed
46
 
1
@@ -27,6 +27,8 @@
2
 {
3
     const DEFAULT_SKIN = 'elastic';
4
 
5
+    public $system_skin = 'elastic';
6
+
7
     private $env       = '';
8
     private $paths     = array();
9
     private $prop      = array();
10
@@ -231,6 +233,8 @@
11
             $this->prop['skin'] = self::DEFAULT_SKIN;
12
         }
13
 
14
+        $this->system_skin = $this->prop['skin'];
15
+
16
         // fix paths
17
         foreach (array('log_dir' => 'logs', 'temp_dir' => 'temp') as $key => $dir) {
18
             foreach (array($this->prop[$key], '../' . $this->prop[$key], RCUBE_INSTALL_PATH . $dir) as $path) {
19
@@ -391,7 +395,7 @@
20
         }
21
         else if ($name == 'client_mimetypes') {
22
             if (!$result && !$def) {
23
-                $result = 'text/plain,text/html,text/xml'
24
+                $result = 'text/plain,text/html'
25
                     . ',image/jpeg,image/gif,image/png,image/bmp,image/tiff,image/webp'
26
                     . ',application/x-javascript,application/pdf,application/x-shockwave-flash';
27
             }
28
@@ -452,7 +456,7 @@
29
         }
30
 
31
         if ($prefs['skin'] == 'default') {
32
-            $prefs['skin'] = self::DEFAULT_SKIN;
33
+            $prefs['skin'] = $this->system_skin;
34
         }
35
 
36
         $skins_allowed = $this->get('skins_allowed');
37
@@ -592,7 +596,7 @@
38
         $list = (array) $this->prop['keyservers'];
39
 
40
         foreach ($list as $idx => $host) {
41
-            if (!preg_match('|^[a-z]://|', $host)) {
42
+            if (!preg_match('|^[a-z]+://|', $host)) {
43
                 $host = "https://$host";
44
             }
45
 
46
iRony-0.4.3.tar.gz/lib/Roundcube/rcube_db.php -> iRony-0.4.4.tar.gz/lib/Roundcube/rcube_db.php Changed
9
 
1
@@ -1264,6 +1264,7 @@
2
             if (!empty($parsed['phptype']) && !empty($parsed['database'])
3
                 && stripos($parsed['phptype'], 'sqlite') === 0
4
                 && $parsed['database'][0] != '/'
5
+                && strpos($parsed['database'], ':') === false
6
             ) {
7
                 $parsed['database'] = INSTALL_PATH . $parsed['database'];
8
             }
9
iRony-0.4.3.tar.gz/lib/Roundcube/rcube_image.php -> iRony-0.4.4.tar.gz/lib/Roundcube/rcube_image.php Changed
90
 
1
@@ -99,7 +99,7 @@
2
     {
3
         $result  = false;
4
         $rcube   = rcube::get_instance();
5
-        $convert = $rcube->config->get('im_convert_path', false);
6
+        $convert = self::getCommand('im_convert_path');
7
         $props   = $this->props();
8
 
9
         if (empty($props)) {
10
@@ -158,7 +158,8 @@
11
                             'size'    => $width . 'x' . $height,
12
                         );
13
 
14
-                        $result = rcube::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace sRGB -strip'
15
+                        $result = rcube::exec($convert
16
+                            . ' 2>&1 -flatten -auto-orient -colorspace sRGB -strip'
17
                             . ' -quality {quality} -resize {size} {intype}:{in} {type}:{out}', $p);
18
                     }
19
                     // use PHP's Imagick class
20
@@ -306,7 +307,7 @@
21
     public function convert($type, $filename = null)
22
     {
23
         $rcube   = rcube::get_instance();
24
-        $convert = $rcube->config->get('im_convert_path', false);
25
+        $convert = self::getCommand('im_convert_path');
26
 
27
         if (!$filename) {
28
             $filename = $this->image_file;
29
@@ -406,7 +407,7 @@
30
         $rcube = rcube::get_instance();
31
 
32
         // @TODO: check if specified mimetype is really supported
33
-        return class_exists('Imagick', false) || $rcube->config->get('im_convert_path');
34
+        return class_exists('Imagick', false) || self::getCommand('im_convert_path');
35
     }
36
 
37
     /**
38
@@ -417,9 +418,9 @@
39
         $rcube = rcube::get_instance();
40
 
41
         // use ImageMagick in command line
42
-        if ($cmd = $rcube->config->get('im_identify_path')) {
43
+        if ($cmd = self::getCommand('im_identify_path')) {
44
             $args = array('in' => $this->image_file, 'format' => "%m %[fx:w] %[fx:h]");
45
-            $id   = rcube::exec($cmd. ' 2>/dev/null -format {format} {in}', $args);
46
+            $id   = rcube::exec($cmd . ' 2>/dev/null -format {format} {in}', $args);
47
 
48
             if ($id) {
49
                 return explode(' ', strtolower($id));
50
@@ -462,4 +463,39 @@
51
         $size = $props['width'] * $props['height'] * $multip;
52
         return rcube_utils::mem_check($size);
53
     }
54
+
55
+    /**
56
+     * Get the configured command and make sure it is safe to use.
57
+     * We cannot trust configuration, and escapeshellcmd() is useless.
58
+     *
59
+     * @param string $opt_name Configuration option name
60
+     *
61
+     * @return bool|string The command or False if not set or invalid
62
+     */
63
+    private static function getCommand($opt_name)
64
+    {
65
+        static $error = [];
66
+
67
+        $cmd = rcube::get_instance()->config->get($opt_name);
68
+
69
+        if (empty($cmd)) {
70
+            return false;
71
+        }
72
+
73
+        if (preg_match('/^(convert|identify)(\.exe)?$/i', $cmd)) {
74
+            return $cmd;
75
+        }
76
+
77
+        // Executable must exist, also disallow network shares on Windows
78
+        if ($cmd[0] != "\\" && file_exists($cmd)) {
79
+            return $cmd;
80
+        }
81
+
82
+        if (empty($error[$opt_name])) {
83
+            rcube::raise_error("Invalid $opt_name: $cmd", true, false);
84
+            $error[$opt_name] = true;
85
+        }
86
+
87
+        return false;
88
+    }
89
 }
90
iRony-0.4.3.tar.gz/lib/Roundcube/rcube_imap.php -> iRony-0.4.4.tar.gz/lib/Roundcube/rcube_imap.php Changed
165
 
1
@@ -1040,32 +1040,28 @@
2
     }
3
 
4
     /**
5
-     * protected method for listing a set of message headers (search results)
6
+     * A protected method for listing a set of message headers (search results)
7
      *
8
-     * @param   string   $folder   Folder name
9
-     * @param   int      $page     Current page to list
10
-     * @param   int      $slice    Number of slice items to extract from result array
11
+     * @param string $folder Folder name
12
+     * @param int    $page   Current page to list
13
+     * @param int    $slice  Number of slice items to extract from the result array
14
      *
15
      * @return array Indexed array with message header objects
16
      */
17
-    protected function list_search_messages($folder, $page, $slice=0)
18
+    protected function list_search_messages($folder, $page, $slice = 0)
19
     {
20
         if (!strlen($folder) || empty($this->search_set) || $this->search_set->is_empty()) {
21
             return array();
22
         }
23
 
24
+        $from = ($page-1) * $this->page_size;
25
+
26
         // gather messages from a multi-folder search
27
         if ($this->search_set->multi) {
28
             $page_size  = $this->page_size;
29
             $sort_field = $this->sort_field;
30
             $search_set = $this->search_set;
31
 
32
-            // prepare paging
33
-            $cnt   = $search_set->count();
34
-            $from  = ($page-1) * $page_size;
35
-            $to    = $from + $page_size;
36
-            $slice_length = min($page_size, $cnt - $from);
37
-
38
             // fetch resultset headers, sort and slice them
39
             if (!empty($sort_field) && $search_set->get_parameters('SORT') != $sort_field) {
40
                 $this->sort_field = null;
41
@@ -1093,7 +1089,7 @@
42
                 $search_set->set_message_index($a_msg_headers, $sort_field, $this->sort_order);
43
 
44
                 // only return the requested part of the set
45
-                $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length);
46
+                $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $page_size);
47
             }
48
             else {
49
                 if ($this->sort_order != $search_set->get_parameters('ORDER')) {
50
@@ -1101,7 +1097,7 @@
51
                 }
52
 
53
                 // slice resultset first...
54
-                $index = array_slice($search_set->get(), $from, $slice_length);
55
+                $index = array_slice($search_set->get(), $from, $page_size);
56
                 $fetch = array();
57
 
58
                 foreach ($index as $msg_id) {
59
@@ -1144,8 +1140,6 @@
60
         }
61
 
62
         $index = clone $this->search_set;
63
-        $from  = ($page-1) * $this->page_size;
64
-        $to    = $from + $this->page_size;
65
 
66
         // return empty array if no messages found
67
         if ($index->is_empty()) {
68
@@ -1178,7 +1172,7 @@
69
             }
70
 
71
             // get messages uids for one page
72
-            $index->slice($from, $to-$from);
73
+            $index->slice($from, $this->page_size);
74
 
75
             if ($slice) {
76
                 $index->slice(-$slice, $slice);
77
@@ -1199,7 +1193,7 @@
78
             // use memory less expensive (and quick) method for big result set
79
             $index = clone $this->index('', $this->sort_field, $this->sort_order);
80
             // get messages uids for one page...
81
-            $index->slice($from, min($cnt-$from, $this->page_size));
82
+            $index->slice($from, $this->page_size);
83
 
84
             if ($slice) {
85
                 $index->slice(-$slice, $slice);
86
@@ -1225,9 +1219,7 @@
87
             $a_msg_headers = rcube_imap_generic::sortHeaders(
88
                 $a_msg_headers, $this->sort_field, $this->sort_order);
89
 
90
-            // only return the requested part of the set
91
-            $slice_length  = min($this->page_size, $cnt - ($to > $cnt ? $from : $to));
92
-            $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length);
93
+            $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $this->page_size);
94
 
95
             if ($slice) {
96
                 $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
97
@@ -1900,8 +1892,11 @@
98
         }
99
 
100
         // Check internal cache
101
-        if (!empty($this->icache['message'])) {
102
-            if (($headers = $this->icache['message']) && $headers->uid == $uid) {
103
+        if (!empty($this->icache['message']) && ($headers = $this->icache['message'])) {
104
+            // Make sure the folder and UID is what we expect.
105
+            // In case when the same process works with folders that are personal
106
+            // and shared two folders can contain the same UIDs.
107
+            if ($headers->uid == $uid && $headers->folder == $folder) {
108
                 return $headers;
109
             }
110
         }
111
@@ -3293,6 +3288,12 @@
112
 
113
         $result = $this->conn->createFolder($folder, $type ? array("\\" . ucfirst($type)) : null);
114
 
115
+        // Folder creation may fail when specific special-use flag is not supported.
116
+        // Try to create it anyway with no flag specified (#7147)
117
+        if (!$result && $type) {
118
+            $result = $this->conn->createFolder($folder);
119
+        }
120
+
121
         // try to subscribe it
122
         if ($result) {
123
             // clear cache
124
@@ -3443,7 +3444,10 @@
125
         $folders = $this->conn->listMailboxes('', '*', array('SUBSCRIBED'), array('SPECIAL-USE'));
126
 
127
         if (!empty($folders)) {
128
-            foreach ($folders as $folder) {
129
+            foreach ($folders as $idx => $folder) {
130
+                if (is_array($folder)) {
131
+                    $folder = $idx;
132
+                }
133
                 if ($flags = $this->conn->data['LIST'][$folder]) {
134
                     foreach ($types as $type) {
135
                         if (in_array($type, $flags)) {
136
@@ -4381,10 +4385,17 @@
137
      */
138
     protected function sort_folder_specials($folder, &$list, &$specials, &$out)
139
     {
140
-        foreach ($list as $key => $name) {
141
+        $count = count($list);
142
+
143
+        for ($i = 0; $i < $count; $i++) {
144
+            $name = $list[$i];
145
+            if ($name === null) {
146
+                continue;
147
+            }
148
+
149
             if ($folder === null || strpos($name, $folder.$this->delimiter) === 0) {
150
                 $out[] = $name;
151
-                unset($list[$key]);
152
+                $list[$i] = null;
153
 
154
                 if (!empty($specials) && ($found = array_search($name, $specials)) !== false) {
155
                     unset($specials[$found]);
156
@@ -4392,8 +4403,6 @@
157
                 }
158
             }
159
         }
160
-
161
-        reset($list);
162
     }
163
 
164
     /**
165
iRony-0.4.3.tar.gz/lib/Roundcube/rcube_imap_generic.php -> iRony-0.4.4.tar.gz/lib/Roundcube/rcube_imap_generic.php Changed
147
 
1
@@ -221,6 +221,38 @@
2
     }
3
 
4
     /**
5
+     * Reads a line of data from the connection stream inluding all
6
+     * string continuation literals.
7
+     *
8
+     * @param int $size Buffer size
9
+     *
10
+     * @return string Line of text response
11
+     */
12
+    protected function readFullLine($size = 1024)
13
+    {
14
+        $line = $this->readLine($size);
15
+
16
+        // include all string literels untile the real end of "line"
17
+        while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
18
+            $bytes = $m[1];
19
+            $out   = '';
20
+
21
+            while (strlen($out) < $bytes) {
22
+                $out = $this->readBytes($bytes);
23
+                if ($out === null) {
24
+                    break;
25
+                }
26
+
27
+                $line .= $out;
28
+            }
29
+
30
+            $line .= $this->readLine($size);
31
+        }
32
+
33
+        return $line;
34
+    }
35
+
36
+    /**
37
      * Reads more data from the connection stream when provided
38
      * data contain string literal
39
      *
40
@@ -1459,7 +1491,7 @@
41
      * @param array  $return_opts (see self::_listMailboxes)
42
      * @param array  $select_opts (see self::_listMailboxes)
43
      *
44
-     * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
45
+     * @return array|bool List of mailboxes or hash of options if STATUS/MYRIGHTS response
46
      *                    is requested, False on error.
47
      */
48
     public function listMailboxes($ref, $mailbox, $return_opts = array(), $select_opts = array())
49
@@ -1474,7 +1506,7 @@
50
      * @param string $mailbox     Mailbox name
51
      * @param array  $return_opts (see self::_listMailboxes)
52
      *
53
-     * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
54
+     * @return array|bool List of mailboxes or hash of options if STATUS/MYRIGHTS response
55
      *                    is requested, False on error.
56
      */
57
     public function listSubscribed($ref, $mailbox, $return_opts = array())
58
@@ -1495,11 +1527,11 @@
59
      *                            Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE,
60
      *                                      SPECIAL-USE (RFC6154)
61
      *
62
-     * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
63
+     * @return array|bool List of mailboxes or hash of options if STATUS/MYRIGHTS response
64
      *                    is requested, False on error.
65
      */
66
-    protected function _listMailboxes($ref, $mailbox, $subscribed=false,
67
-        $return_opts=array(), $select_opts=array())
68
+    protected function _listMailboxes($ref, $mailbox, $subscribed = false,
69
+        $return_opts = array(), $select_opts = array())
70
     {
71
         if (!strlen($mailbox)) {
72
             $mailbox = '*';
73
@@ -1710,7 +1742,7 @@
74
      *
75
      * @param array $items Client identification information key/value hash
76
      *
77
-     * @return array Server identification information key/value hash
78
+     * @return array|false Server identification information key/value hash, False on error
79
      * @since 0.6
80
      */
81
     public function id($items = array())
82
@@ -1729,10 +1761,12 @@
83
         if ($code == self::ERROR_OK && $response) {
84
             $response = substr($response, 5); // remove prefix "* ID "
85
             $items    = $this->tokenizeResponse($response, 1);
86
-            $result   = null;
87
+            $result   = array();
88
 
89
-            for ($i=0, $len=count($items); $i<$len; $i += 2) {
90
-                $result[$items[$i]] = $items[$i+1];
91
+            if (is_array($items)) {
92
+                for ($i=0, $len=count($items); $i<$len; $i += 2) {
93
+                    $result[$items[$i]] = $items[$i+1];
94
+                }
95
             }
96
 
97
             return $result;
98
@@ -2397,7 +2431,7 @@
99
         }
100
 
101
         do {
102
-            $line = $this->readLine(4096);
103
+            $line = $this->readFullLine(4096);
104
 
105
             if (!$line) {
106
                 break;
107
@@ -2421,27 +2455,6 @@
108
                 $line    = substr($line, strlen($m[0]) + 2);
109
                 $ln      = 0;
110
 
111
-                // get complete entry
112
-                while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
113
-                    $bytes = $m[1];
114
-                    $out   = '';
115
-
116
-                    while (strlen($out) < $bytes) {
117
-                        $out = $this->readBytes($bytes);
118
-                        if ($out === null) {
119
-                            break;
120
-                        }
121
-                        $line .= $out;
122
-                    }
123
-
124
-                    $str = $this->readLine(4096);
125
-                    if ($str === false) {
126
-                        break;
127
-                    }
128
-
129
-                    $line .= $str;
130
-                }
131
-
132
                 // Tokenize response and assign to object properties
133
                 while (list($name, $value) = $this->tokenizeResponse($line, 2)) {
134
                     if ($name == 'UID') {
135
@@ -3721,10 +3734,9 @@
136
 
137
         // Parse response
138
         do {
139
-            $line = $this->readLine(4096);
140
+            $line = $this->readFullLine(4096);
141
 
142
             if ($response !== null) {
143
-                // TODO: Better string literals handling with filter
144
                 if (!$filter || preg_match($filter, $line)) {
145
                     $response .= $line;
146
                 }
147
iRony-0.4.3.tar.gz/lib/Roundcube/rcube_message.php -> iRony-0.4.4.tar.gz/lib/Roundcube/rcube_message.php Changed
42
 
1
@@ -124,7 +124,7 @@
2
         }
3
 
4
         $this->mime    = new rcube_mime($this->headers->charset);
5
-        $this->subject = $this->headers->get('subject');
6
+        $this->subject = str_replace("\n", '', $this->headers->get('subject'));
7
         $from          = $this->mime->decode_address_list($this->headers->from, 1);
8
         $this->sender  = current($from);
9
 
10
@@ -525,7 +525,7 @@
11
     public function is_attachment($part)
12
     {
13
         foreach ($this->attachments as $att_part) {
14
-            if ($att_part->mime_id == $part->mime_id) {
15
+            if ($att_part->mime_id === $part->mime_id) {
16
                 return true;
17
             }
18
 
19
@@ -580,7 +580,7 @@
20
                 list($headers, ) = explode("\r\n\r\n", $this->get_part_body($structure->mime_id, false, 32768));
21
                 $structure->headers = rcube_mime::parse_headers($headers);
22
 
23
-                if ($this->context == $structure->mime_id) {
24
+                if ($this->context === $structure->mime_id) {
25
                     $this->headers = rcube_message_header::from_array($structure->headers);
26
                 }
27
             }
28
@@ -878,6 +878,13 @@
29
 
30
                     $this->add_part($mail_part, 'attachment');
31
                 }
32
+                // Last resort, non-inline and non-text part of multipart/mixed message (#7117)
33
+                else if ($mimetype == 'multipart/mixed'
34
+                    && $mail_part->disposition != 'inline'
35
+                    && $primary_type && $primary_type != 'text' && $primary_type != 'multipart'
36
+                ) {
37
+                    $this->add_part($mail_part, 'attachment');
38
+                }
39
             }
40
 
41
             // if this is a related part try to resolve references
42
iRony-0.4.3.tar.gz/lib/Roundcube/rcube_plugin_api.php -> iRony-0.4.4.tar.gz/lib/Roundcube/rcube_plugin_api.php Changed
77
 
1
@@ -164,6 +164,14 @@
2
             $plugins_dir = unslashify($dir->path);
3
         }
4
 
5
+        // Validate the plugin name to prevent from path traversal
6
+        if (preg_match('/[^a-zA-Z0-9_-]/', $plugin_name)) {
7
+            rcube::raise_error(array('code' => 520,
8
+                    'file' => __FILE__, 'line' => __LINE__,
9
+                    'message' => "Invalid plugin name: $plugin_name"), true, false);
10
+            return false;
11
+        }
12
+
13
         // plugin already loaded?
14
         if (!$this->plugins[$plugin_name]) {
15
             $fn = "$plugins_dir/$plugin_name/$plugin_name.php";
16
@@ -237,7 +245,7 @@
17
 
18
     /**
19
      * Get information about a specific plugin.
20
-     * This is either provided my a plugin's info() method or extracted from a package.xml or a composer.json file
21
+     * This is either provided by a plugin's info() method or extracted from a package.xml or a composer.json file
22
      *
23
      * @param string Plugin name
24
      * @return array Meta information about a plugin or False if plugin was not found
25
@@ -283,6 +291,14 @@
26
         $fn   = unslashify($dir->path) . "/$plugin_name/$plugin_name.php";
27
         $info = false;
28
 
29
+        // Validate the plugin name to prevent from path traversal
30
+        if (preg_match('/[^a-zA-Z0-9_-]/', $plugin_name)) {
31
+            rcube::raise_error(array('code' => 520,
32
+                    'file' => __FILE__, 'line' => __LINE__,
33
+                    'message' => "Invalid plugin name: $plugin_name"), true, false);
34
+            return false;
35
+        }
36
+
37
         if (!class_exists($plugin_name, false)) {
38
             if (is_readable($fn)) {
39
                 include($fn);
40
@@ -300,17 +316,29 @@
41
         if (!$info) {
42
             $composer = INSTALL_PATH . "/plugins/$plugin_name/composer.json";
43
             if (is_readable($composer) && ($json = @json_decode(file_get_contents($composer), true))) {
44
+                // Build list of plugins required
45
+                $require = array();
46
+                foreach (array_keys((array) $json['require']) as $dname) {
47
+                    if (!preg_match('|^([^/]+)/([a-zA-Z0-9_-]+)$|', $dname, $m)) {
48
+                        continue;
49
+                    }
50
+
51
+                    $vendor = $m[1];
52
+                    $name = $m[2];
53
+
54
+                    if ($name != 'plugin-installer' && $vendor != 'pear' && $vendor != 'pear-pear') {
55
+                        $dpath = unslashify($dir->path) . "/$name/$name.php";
56
+                        if (is_readable($dpath)) {
57
+                            $require[] = $name;
58
+                        }
59
+                    }
60
+                }
61
+
62
                 list($info['vendor'], $info['name']) = explode('/', $json['name']);
63
                 $info['version'] = $json['version'];
64
                 $info['license'] = $json['license'];
65
                 $info['uri']     = $json['homepage'];
66
-                $info['require'] = array_filter(array_keys((array)$json['require']), function($pname) {
67
-                    if (strpos($pname, '/') == false) {
68
-                        return false;
69
-                    }
70
-                    list($vendor, $name) = explode('/', $pname);
71
-                    return !($name == 'plugin-installer' || $vendor == 'pear-pear');
72
-                });
73
+                $info['require'] = $require;
74
             }
75
 
76
             // read local composer.lock file (once)
77
iRony-0.4.3.tar.gz/lib/Roundcube/rcube_string_replacer.php -> iRony-0.4.4.tar.gz/lib/Roundcube/rcube_string_replacer.php Changed
16
 
1
@@ -59,10 +59,10 @@
2
         $this->linkref_pattern = '/\[([^\]#]+)\]/';
3
         $this->link_pattern    = "/$link_prefix($utf_domain([$url1]*[$url2]+)*)/";
4
         $this->mailto_pattern  = "/("
5
-            ."[-\w!\#\$%&\'*+~\/^`|{}=]+(?:\.[-\w!\#\$%&\'*+~\/^`|{}=]+)*"  // local-part
6
-            ."@$utf_domain"                                                 // domain-part
7
-            ."(\?[$url1$url2]+)?"                                           // e.g. ?subject=test...
8
-            .")/";
9
+            . "[-\w!\#\$%&*+~\/^`|{}=]+(?:\.[-\w!\#\$%&*+~\/^`|{}=]+)*"  // local-part
10
+            . "@$utf_domain"                                             // domain-part
11
+            . "(\?[$url1$url2]+)?"                                       // e.g. ?subject=test...
12
+            . ")/";
13
     }
14
 
15
     /**
16
iRony-0.4.3.tar.gz/lib/Roundcube/rcube_utils.php -> iRony-0.4.4.tar.gz/lib/Roundcube/rcube_utils.php Changed
11
 
1
@@ -398,6 +398,9 @@
2
             // Convert position:fixed to position:absolute (#5264)
3
             $styles = preg_replace('/position[^a-z]*:[\s\r\n]*fixed/i', 'position: absolute', $styles);
4
 
5
+            // Remove 'page' attributes (#7604)
6
+            $styles = preg_replace('/(^|[\n\s;])page:[^;]+;*/im', '', $styles);
7
+
8
             // check every line of a style block...
9
             if ($allow_remote) {
10
                 $a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY);
11
iRony-0.4.3.tar.gz/lib/Roundcube/rcube_vcard.php -> iRony-0.4.4.tar.gz/lib/Roundcube/rcube_vcard.php Changed
12
 
1
@@ -240,6 +240,10 @@
2
                         list(,, $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']) = $raw;
3
                         $out[$key][] = $value;
4
                     }
5
+                    // support vCard v4 date format (YYYYMMDD)
6
+                    else if ($tag == 'BDAY' && preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $raw[0], $m)) {
7
+                        $out[$key][] = sprintf('%04d-%02d-%02d', intval($m[1]), intval($m[2]), intval($m[3]));
8
+                    }
9
                     else {
10
                         $out[$key][] = $raw[0];
11
                     }
12
iRony-0.4.3.tar.gz/lib/Roundcube/rcube_washtml.php -> iRony-0.4.4.tar.gz/lib/Roundcube/rcube_washtml.php Changed
199
 
1
@@ -330,11 +330,7 @@
2
                     $out = $this->wash_uri($value, true);
3
                 }
4
                 else if ($this->is_link_attribute($node->nodeName, $key)) {
5
-                    if (!preg_match('!^(javascript|vbscript|data:)!i', $value)
6
-                        && preg_match('!^([a-z][a-z0-9.+-]+:|//|#).+!i', $value)
7
-                    ) {
8
-                        $out = $value;
9
-                    }
10
+                    $out = $this->wash_link($value);
11
                 }
12
                 else if ($this->is_funciri_attribute($node->nodeName, $key)) {
13
                     if (preg_match('/^[a-z:]*url\(/i', $val)) {
14
@@ -399,6 +395,10 @@
15
 
16
         // allow url(#id) used in SVG
17
         if ($uri[0] == '#') {
18
+            if ($this->_css_prefix !== null) {
19
+                $uri = '#' . $this->_css_prefix . substr($uri, 1);
20
+            }
21
+
22
             return $uri;
23
         }
24
 
25
@@ -412,12 +412,59 @@
26
                 return $this->config['blocked_src'];
27
             }
28
         }
29
-        else if ($is_image && preg_match('/^data:image.+/i', $uri)) { // RFC2397
30
+        else if ($is_image && preg_match('/^data:image\/([^,]+),(.+)$/i', $uri, $matches)) { // RFC2397
31
+            // svg images can be insecure, we'll sanitize them
32
+            if (stripos($matches[1], 'svg') !== false) {
33
+                $svg = $matches[2];
34
+
35
+                if (stripos($matches[1], ';base64') !== false) {
36
+                    $svg  = base64_decode($svg);
37
+                    $type = $matches[1];
38
+                }
39
+                else {
40
+                    $type = $matches[1] . ';base64';
41
+                }
42
+
43
+                $washer = new self($this->config);
44
+                $svg    = $washer->wash($svg);
45
+
46
+                // Invalid svg content
47
+                if (empty($svg)) {
48
+                    return null;
49
+                }
50
+
51
+                return 'data:image/' . $type . ',' . base64_encode($svg);
52
+            }
53
+
54
             return $uri;
55
         }
56
     }
57
 
58
     /**
59
+     * Wash Href value
60
+     *
61
+     * @param string $href Href attribute value (link)
62
+     *
63
+     * @return string Washed href
64
+     */
65
+    private function wash_link($href)
66
+    {
67
+        if (strlen($href) && !preg_match('!^(javascript|vbscript|data:)!i', $href)) {
68
+            if ($href[0] == '#' && $this->_css_prefix !== null) {
69
+                return '#' . $this->_css_prefix . substr($href, 1);
70
+            }
71
+
72
+            if (preg_match('!^[a-zA-Z._-]+$!', $href)) {
73
+                return 'http://' . $href;
74
+            }
75
+
76
+            if (preg_match('!^([a-z][a-z0-9.+-]+:|//|#).+!i', $href)) {
77
+                return $href;
78
+            }
79
+        }
80
+    }
81
+
82
+    /**
83
      * Check it the tag/attribute may contain an URI
84
      *
85
      * @param string $tag  Element name
86
@@ -427,7 +474,7 @@
87
      */
88
     private function is_link_attribute($tag, $attr)
89
     {
90
-        return ($tag == 'a' || $tag == 'area') && $attr == 'href';
91
+        return $attr === 'href';
92
     }
93
 
94
     /**
95
@@ -444,6 +491,7 @@
96
             || $attr == 'color-profile' // SVG
97
             || ($attr == 'poster' && $tag == 'video')
98
             || ($attr == 'src' && preg_match('/^(img|image|source|input|video|audio)$/i', $tag))
99
+            || ($tag == 'use' && $attr == 'href') // SVG
100
             || ($tag == 'image' && $attr == 'href'); // SVG
101
     }
102
 
103
@@ -462,6 +510,31 @@
104
     }
105
 
106
     /**
107
+     * Check if a specified element has an attribute with specified value.
108
+     * Do it in case-insensitive manner.
109
+     *
110
+     * @param DOMElement $node       The element
111
+     * @param string     $attr_name  The attribute name
112
+     * @param string     $attr_value The attribute value to find
113
+     *
114
+     * @return bool True if the specified attribute exists and has the expected value
115
+     */
116
+    private static function attribute_value($node, $attr_name, $attr_value)
117
+    {
118
+        $attr_name = strtolower($attr_name);
119
+
120
+        foreach ($node->attributes as $name => $attr) {
121
+            if (strtolower($name) === $attr_name) {
122
+                if (strtolower($attr_value) === strtolower($attr->nodeValue)) {
123
+                    return true;
124
+                }
125
+            }
126
+        }
127
+
128
+        return false;
129
+    }
130
+
131
+    /**
132
      * The main loop that recurse on a node tree.
133
      * It output only allowed tags with allowed attributes and allowed inline styles
134
      *
135
@@ -508,6 +581,13 @@
136
 
137
                     $node->setAttribute('href', (string) $uri);
138
                 }
139
+                else if (in_array($tagName, array('animate', 'animatecolor', 'set', 'animatetransform'))
140
+                    && self::attribute_value($node, 'attributename', 'href')
141
+                ) {
142
+                    // Insecure svg tags
143
+                    $dump .= "<!-- $tagName blocked -->";
144
+                    break;
145
+                }
146
 
147
                 if ($callback = $this->handlers[$tagName]) {
148
                     $dump .= call_user_func($callback, $tagName,
149
@@ -521,7 +601,10 @@
150
                         $xpath = new DOMXPath($node->ownerDocument);
151
                         foreach ($xpath->query('namespace::*') as $ns) {
152
                             if ($ns->nodeName != 'xmlns:xml') {
153
-                                $dump .= ' ' . $ns->nodeName . '="' . $ns->nodeValue . '"';
154
+                                $dump .= sprintf(' %s="%s"',
155
+                                    $ns->nodeName,
156
+                                    htmlspecialchars($ns->nodeValue, ENT_QUOTES, $this->config['charset'])
157
+                                );
158
                             }
159
                         }
160
                     }
161
@@ -548,9 +631,6 @@
162
                 break;
163
 
164
             case XML_CDATA_SECTION_NODE:
165
-                $dump .= $node->nodeValue;
166
-                break;
167
-
168
             case XML_TEXT_NODE:
169
                 $dump .= htmlspecialchars($node->nodeValue, ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, $this->config['charset']);
170
                 break;
171
@@ -591,11 +671,15 @@
172
         $this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level');
173
 
174
         // SVG need to be parsed as XML
175
-        $this->is_xml = stripos($html, '<html') === false && stripos($html, '<svg') !== false;
176
+        $this->is_xml = !preg_match('/<(html|head|body)/i', $html) && stripos($html, '<svg') !== false;
177
         $method       = $this->is_xml ? 'loadXML' : 'loadHTML';
178
 
179
         // DOMDocument does not support HTML5, try Masterminds parser if available
180
-        if (!$this->is_xml && class_exists('Masterminds\HTML5')) {
181
+        if (!$this->is_xml && class_exists('Masterminds\HTML5')
182
+            // HTML5 parser is slow with content that contains a lot of tags
183
+            // disable it for such cases (https://github.com/Masterminds/html5-php/issues/181)
184
+            && substr_count($html, '<') < 10000
185
+        ) {
186
             try {
187
                 $html5 = new Masterminds\HTML5();
188
                 $node  = $html5->loadHTML($this->fix_html5($html));
189
@@ -650,7 +734,8 @@
190
             // washtml/DOMDocument cannot handle xml namespaces
191
             '/<html\s[^>]+>/i',
192
             // washtml/DOMDocument cannot handle xml namespaces
193
-            '/<\?xml:namespace\s[^>]+>/i',
194
+            // HTML5 parser cannot handler <?xml
195
+            '/<\?xml[^>]*>/i',
196
         );
197
 
198
         $html_replace = array(
199
iRony-0.4.3.tar.gz/lib/plugins/calendar/calendar.php -> iRony-0.4.4.tar.gz/lib/plugins/calendar/calendar.php Changed
163
 
1
@@ -1313,20 +1313,11 @@
2
    */
3
   function load_events()
4
   {
5
-    $start  = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET);
6
-    $end    = rcube_utils::get_input_value('end', rcube_utils::INPUT_GET);
7
+    $start  = $this->input_timestamp('start', rcube_utils::INPUT_GET);
8
+    $end    = $this->input_timestamp('end', rcube_utils::INPUT_GET);
9
     $query  = rcube_utils::get_input_value('q', rcube_utils::INPUT_GET);
10
     $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET);
11
 
12
-    if (!is_numeric($start) || strpos($start, 'T')) {
13
-      $start = new DateTime($start, $this->timezone);
14
-      $start = $start->getTimestamp();
15
-    }
16
-    if (!is_numeric($end) || strpos($end, 'T')) {
17
-      $end = new DateTime($end, $this->timezone);
18
-      $end = $end->getTimestamp();
19
-    }
20
-
21
     $events = $this->driver->load_events($start, $end, $query, $source);
22
     echo $this->encode($events, !empty($query));
23
     exit;
24
@@ -2282,31 +2273,21 @@
25
   public function freebusy_status()
26
   {
27
     $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC);
28
-    $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC);
29
-    $end   = rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC);
30
+    $start = $this->input_timestamp('start', rcube_utils::INPUT_GPC);
31
+    $end   = $this->input_timestamp('end', rcube_utils::INPUT_GPC);
32
 
33
-    // convert dates into unix timestamps
34
-    if (!empty($start) && !is_numeric($start)) {
35
-      $dts = new DateTime($start, $this->timezone);
36
-      $start = $dts->format('U');
37
-    }
38
-    if (!empty($end) && !is_numeric($end)) {
39
-      $dte = new DateTime($end, $this->timezone);
40
-      $end = $dte->format('U');
41
-    }
42
-    
43
     if (!$start) $start = time();
44
     if (!$end) $end = $start + 3600;
45
-    
46
+
47
     $fbtypemap = array(calendar::FREEBUSY_UNKNOWN => 'UNKNOWN', calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE');
48
     $status = 'UNKNOWN';
49
-    
50
+
51
     // if the backend has free-busy information
52
     $fblist = $this->driver->get_freebusy_list($email, $start, $end);
53
 
54
     if (is_array($fblist)) {
55
       $status = 'FREE';
56
-      
57
+
58
       foreach ($fblist as $slot) {
59
         list($from, $to, $type) = $slot;
60
         if ($from < $end && $to > $start) {
61
@@ -2315,14 +2296,14 @@
62
         }
63
       }
64
     }
65
-    
66
+
67
     // let this information be cached for 5min
68
     $this->rc->output->future_expire_header(300);
69
-    
70
+
71
     echo $status;
72
     exit;
73
   }
74
-  
75
+
76
   /**
77
    * Return a list of free/busy time slots within the given period
78
    * Echo data in JSON encoding
79
@@ -2330,25 +2311,15 @@
80
   public function freebusy_times()
81
   {
82
     $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC);
83
-    $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC);
84
-    $end   = rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC);
85
+    $start = $this->input_timestamp('start', rcube_utils::INPUT_GPC);
86
+    $end   = $this->input_timestamp('end', rcube_utils::INPUT_GPC);
87
     $interval  = intval(rcube_utils::get_input_value('interval', rcube_utils::INPUT_GPC));
88
     $strformat = $interval > 60 ? 'Ymd' : 'YmdHis';
89
 
90
-    // convert dates into unix timestamps
91
-    if (!empty($start) && !is_numeric($start)) {
92
-      $dts = rcube_utils::anytodatetime($start, $this->timezone);
93
-      $start = $dts ? $dts->format('U') : null;
94
-    }
95
-    if (!empty($end) && !is_numeric($end)) {
96
-      $dte = rcube_utils::anytodatetime($end, $this->timezone);
97
-      $end = $dte ? $dte->format('U') : null;
98
-    }
99
-
100
     if (!$start) $start = time();
101
     if (!$end)   $end = $start + 86400 * 30;
102
     if (!$interval) $interval = 60;  // 1 hour
103
-    
104
+
105
     if (!$dte) {
106
       $dts = new DateTime('@'.$start);
107
       $dts->setTimezone($this->timezone);
108
@@ -2399,13 +2370,13 @@
109
       $slots .= $status;
110
       $t = $t_end;
111
     }
112
-    
113
+
114
     $dte = new DateTime('@'.$t_end);
115
     $dte->setTimezone($this->timezone);
116
-    
117
+
118
     // let this information be cached for 5min
119
     $this->rc->output->future_expire_header(300);
120
-    
121
+
122
     echo rcube_output::json_serialize(array(
123
       'email' => $email,
124
       'start' => $dts->format('c'),
125
@@ -2599,10 +2570,11 @@
126
     $events = array();
127
 
128
     if ($directory = $this->resources_directory()) {
129
-      $events = $directory->get_resource_calendar(
130
-        rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC),
131
-        rcube_utils::get_input_value('start', rcube_utils::INPUT_GET),
132
-        rcube_utils::get_input_value('end', rcube_utils::INPUT_GET));
133
+      $id    = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
134
+      $start = $this->input_timestamp('start', rcube_utils::INPUT_GET);
135
+      $end   = $this->input_timestamp('end', rcube_utils::INPUT_GET);
136
+
137
+      $events = $directory->get_resource_calendar($id, $start, $end);
138
     }
139
 
140
     echo $this->encode($events);
141
@@ -3588,6 +3560,21 @@
142
   }
143
 
144
   /**
145
+   * Get date-time input from UI and convert to unix timestamp
146
+   */
147
+  protected function input_timestamp($name, $type)
148
+  {
149
+    $ts = rcube_utils::get_input_value($name, $type);
150
+
151
+    if ($ts && (!is_numeric($ts) || strpos($ts, 'T'))) {
152
+      $ts = new DateTime($ts, $this->timezone);
153
+      $ts = $ts->getTimestamp();
154
+    }
155
+
156
+    return $ts;
157
+  }
158
+
159
+  /**
160
    * Magic getter for public access to protected members
161
    */
162
   public function __get($name)
163
iRony-0.4.3.tar.gz/lib/plugins/calendar/calendar_ui.js -> iRony-0.4.4.tar.gz/lib/plugins/calendar/calendar_ui.js Changed
81
 
1
@@ -563,6 +563,11 @@
2
         }
3
       });
4
 
5
+      var close_func = function(e) {
6
+          rcmail.command('menu-close', 'eventoptionsmenu', null, e);
7
+          $('.libcal-rsvp-replymode').hide();
8
+        };
9
+
10
       // open jquery UI dialog
11
       $dialog.dialog({
12
         modal: true,
13
@@ -575,21 +580,12 @@
14
             $dialog.parent().find('button:not(.ui-dialog-titlebar-close,.delete)').first().focus();
15
           }, 5);
16
         },
17
-        beforeClose: function(e) {
18
-          rcmail.command('menu-close', 'eventoptionsmenu', null, e);
19
-        },
20
         close: function(e) {
21
-          $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
22
-          $('.libcal-rsvp-replymode').hide();
23
-        },
24
-        dragStart: function(e) {
25
-          rcmail.command('menu-close', 'eventoptionsmenu', null, e);
26
-          $('.libcal-rsvp-replymode').hide();
27
-        },
28
-        resizeStart: function(e) {
29
-          rcmail.command('menu-close', 'eventoptionsmenu', null, e);
30
-          $('.libcal-rsvp-replymode').hide();
31
+          close_func(e);
32
+          $dialog.dialog('close');
33
         },
34
+        dragStart: close_func,
35
+        resizeStart: close_func,
36
         buttons: buttons,
37
         minWidth: 320,
38
         width: 420
39
@@ -611,12 +607,12 @@
40
           .attr({href: '#', 'class': 'dropdown-link btn btn-link options', 'data-popup-pos': 'top'})
41
           .text(rcmail.gettext('eventoptions','calendar'))
42
           .click(function(e) {
43
-            return rcmail.command('menu-open','eventoptionsmenu', this, e)
44
+            return rcmail.command('menu-open','eventoptionsmenu', this, e);
45
           })
46
           .appendTo($dialog.parent().find('.ui-dialog-buttonset'));
47
       }
48
 
49
-      rcmail.enable_command('event-history', calendar.history)
50
+      rcmail.enable_command('event-history', calendar.history);
51
 
52
       rcmail.triggerEvent('calendar-event-dialog', {dialog: $dialog});
53
     };
54
@@ -626,12 +622,14 @@
55
     {
56
       var cutype = $(this).attr('data-cutype'),
57
         mailto = this.href.substr(7);
58
+
59
       if (rcmail.env.calendar_resources && cutype == 'RESOURCE') {
60
         event_resources_dialog(mailto);
61
       }
62
       else {
63
         rcmail.command('compose', mailto, e ? e.target : null, e);
64
       }
65
+
66
       return false;
67
     };
68
 
69
@@ -3179,9 +3177,9 @@
70
           if (v.role != 'ORGANIZER') {
71
             v.status = 'NEEDS-ACTION';
72
           }
73
-        })
74
+        });
75
 
76
-        event_edit_dialog('new', copy);
77
+        setTimeout(function() { event_edit_dialog('new', copy); }, 50);
78
       }
79
     };
80
 
81
iRony-0.4.3.tar.gz/lib/plugins/calendar/composer.json -> iRony-0.4.4.tar.gz/lib/plugins/calendar/composer.json Changed
10
 
1
@@ -4,7 +4,7 @@
2
     "description": "Calendar plugin",
3
     "homepage": "https://git.kolab.org/diffusion/RPK/",
4
     "license": "AGPLv3",
5
-    "version": "3.5.2",
6
+    "version": "3.5.5",
7
     "authors": [
8
         {
9
             "name": "Thomas Bruederli",
10
iRony-0.4.3.tar.gz/lib/plugins/calendar/drivers/kolab/kolab_driver.php -> iRony-0.4.4.tar.gz/lib/plugins/calendar/drivers/kolab/kolab_driver.php Changed
10
 
1
@@ -2375,7 +2375,7 @@
2
     if (strlen($folder)) {
3
       $path_imap = explode($delim, $folder);
4
       array_pop($path_imap);  // pop off name part
5
-      $path_imap = implode($path_imap, $delim);
6
+      $path_imap = implode($delim, $path_imap);
7
 
8
       $options = $storage->folder_info($folder);
9
     }
10
iRony-0.4.3.tar.gz/lib/plugins/calendar/lib/js/fullcalendar.js -> iRony-0.4.4.tar.gz/lib/plugins/calendar/lib/js/fullcalendar.js Changed
20516
 
1
@@ -1,7 +1,7 @@
2
 /*!
3
- * FullCalendar v3.9.0
4
+ * FullCalendar v3.10.2
5
  * Docs & License: https://fullcalendar.io/
6
- * (c) 2018 Adam Shaw
7
+ * (c) 2019 Adam Shaw
8
  */
9
 (function webpackUniversalModuleDefinition(root, factory) {
10
    if(typeof exports === 'object' && typeof module === 'object')
11
@@ -75,7 +75,7 @@
12
 /******/   __webpack_require__.p = "";
13
 /******/
14
 /******/   // Load entry module and return exports
15
-/******/   return __webpack_require__(__webpack_require__.s = 236);
16
+/******/   return __webpack_require__(__webpack_require__.s = 256);
17
 /******/ })
18
 /************************************************************************/
19
 /******/ ([
20
@@ -200,7 +200,7 @@
21
         var naturalOffset = flexOffsets[i];
22
         var naturalHeight = flexHeights[i];
23
         var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
24
-        if (naturalOffset < minOffset) {
25
+        if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
26
             $(el).height(newHeight);
27
         }
28
     });
29
@@ -314,7 +314,7 @@
30
     leftRightWidth = sanitizeScrollbarWidth(leftRightWidth);
31
     bottomWidth = sanitizeScrollbarWidth(bottomWidth);
32
     widths = { left: 0, right: 0, top: 0, bottom: bottomWidth };
33
-    if (getIsLeftRtlScrollbars() && el.css('direction') === 'rtl') {
34
+    if (getIsLeftRtlScrollbars() && el.css('direction') === 'rtl') { // is the scrollbar on the left side?
35
         widths.left = leftRightWidth;
36
     }
37
     else {
38
@@ -339,7 +339,7 @@
39
     return _isLeftRtlScrollbars;
40
 }
41
 function computeIsLeftRtlScrollbars() {
42
-    var el = $('<div><div/></div>')
43
+    var el = $('<div><div></div></div>')
44
         .css({
45
         position: 'absolute',
46
         top: -1000,
47
@@ -577,13 +577,13 @@
48
 // Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
49
 // of month-diffing logic (which tends to vary from version to version).
50
 function computeRangeAs(unit, start, end) {
51
-    if (end != null) {
52
+    if (end != null) { // given start, end
53
         return end.diff(start, unit, true);
54
     }
55
-    else if (moment.isDuration(start)) {
56
+    else if (moment.isDuration(start)) { // given duration
57
         return start.as(unit);
58
     }
59
-    else {
60
+    else { // given { start, end } range object
61
         return start.end.diff(start.start, unit, true);
62
     }
63
 }
64
@@ -709,7 +709,7 @@
65
     for (i = propObjs.length - 1; i >= 0; i--) {
66
         props = propObjs[i];
67
         for (name in props) {
68
-            if (!(name in dest)) {
69
+            if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign
70
                 dest[name] = props[name];
71
             }
72
         }
73
@@ -747,7 +747,7 @@
74
     var removeCnt = 0;
75
     var i = 0;
76
     while (i < array.length) {
77
-        if (testFunc(array[i])) {
78
+        if (testFunc(array[i])) { // truthy value means *remove*
79
             array.splice(i, 1);
80
             removeCnt++;
81
         }
82
@@ -776,7 +776,7 @@
83
 function isArraysEqual(a0, a1) {
84
     var len = a0.length;
85
     var i;
86
-    if (len == null || len !== a1.length) {
87
+    if (len == null || len !== a1.length) { // not array? or not same length?
88
         return false;
89
     }
90
     for (i = 0; i < len; i++) {
91
@@ -805,7 +805,7 @@
92
         .replace(/>/g, '&gt;')
93
         .replace(/'/g, '&#039;')
94
         .replace(/"/g, '&quot;')
95
-        .replace(/\n/g, '<br />');
96
+        .replace(/\n/g, '<br>');
97
 }
98
 exports.htmlEscape = htmlEscape;
99
 function stripHtmlEntities(text) {
100
@@ -907,7 +907,7 @@
101
 
102
 Object.defineProperty(exports, "__esModule", { value: true });
103
 var moment = __webpack_require__(0);
104
-var moment_ext_1 = __webpack_require__(10);
105
+var moment_ext_1 = __webpack_require__(11);
106
 var UnzonedRange = /** @class */ (function () {
107
     function UnzonedRange(startInput, endInput) {
108
         // TODO: move these into footprint.
109
@@ -942,7 +942,7 @@
110
         for (i = 0; i < ranges.length; i++) {
111
             dateRange = ranges[i];
112
             // add the span of time before the event (if there is any)
113
-            if (dateRange.startMs > startMs) {
114
+            if (dateRange.startMs > startMs) { // compare millisecond time (skip any ambig logic)
115
                 invertedRanges.push(new UnzonedRange(startMs, dateRange.startMs));
116
             }
117
             if (dateRange.endMs > startMs) {
118
@@ -950,7 +950,7 @@
119
             }
120
         }
121
         // add the span of time after the last event (if there is any)
122
-        if (startMs < constraintRange.endMs) {
123
+        if (startMs < constraintRange.endMs) { // compare millisecond time (skip any ambig logic)
124
             invertedRanges.push(new UnzonedRange(startMs, constraintRange.endMs));
125
         }
126
         return invertedRanges;
127
@@ -1058,9 +1058,9 @@
128
 Object.defineProperty(exports, "__esModule", { value: true });
129
 var tslib_1 = __webpack_require__(2);
130
 var $ = __webpack_require__(3);
131
-var ParsableModelMixin_1 = __webpack_require__(208);
132
-var Class_1 = __webpack_require__(33);
133
-var EventDefParser_1 = __webpack_require__(49);
134
+var ParsableModelMixin_1 = __webpack_require__(52);
135
+var Class_1 = __webpack_require__(35);
136
+var EventDefParser_1 = __webpack_require__(36);
137
 var EventSource = /** @class */ (function (_super) {
138
     tslib_1.__extends(EventSource, _super);
139
     // can we do away with calendar? at least for the abstract?
140
@@ -1189,7 +1189,7 @@
141
 Object.defineProperty(exports, "__esModule", { value: true });
142
 var tslib_1 = __webpack_require__(2);
143
 var $ = __webpack_require__(3);
144
-var Mixin_1 = __webpack_require__(14);
145
+var Mixin_1 = __webpack_require__(15);
146
 var guid = 0;
147
 var ListenerMixin = /** @class */ (function (_super) {
148
     tslib_1.__extends(ListenerMixin, _super);
149
@@ -1208,7 +1208,7 @@
150
       })
151
     */
152
     ListenerMixin.prototype.listenTo = function (other, arg, callback) {
153
-        if (typeof arg === 'object') {
154
+        if (typeof arg === 'object') { // given dictionary of callbacks
155
             for (var eventName in arg) {
156
                 if (arg.hasOwnProperty(eventName)) {
157
                     this.listenTo(other, eventName, arg[eventName]);
158
@@ -1246,8 +1246,76 @@
159
 
160
 /***/ }),
161
 /* 8 */,
162
-/* 9 */,
163
-/* 10 */
164
+/* 9 */
165
+/***/ (function(module, exports, __webpack_require__) {
166
+
167
+Object.defineProperty(exports, "__esModule", { value: true });
168
+var tslib_1 = __webpack_require__(2);
169
+var EventDef_1 = __webpack_require__(37);
170
+var EventInstance_1 = __webpack_require__(53);
171
+var EventDateProfile_1 = __webpack_require__(16);
172
+var SingleEventDef = /** @class */ (function (_super) {
173
+    tslib_1.__extends(SingleEventDef, _super);
174
+    function SingleEventDef() {
175
+        return _super !== null && _super.apply(this, arguments) || this;
176
+    }
177
+    /*
178
+    Will receive start/end params, but will be ignored.
179
+    */
180
+    SingleEventDef.prototype.buildInstances = function () {
181
+        return [this.buildInstance()];
182
+    };
183
+    SingleEventDef.prototype.buildInstance = function () {
184
+        return new EventInstance_1.default(this, // definition
185
+        this.dateProfile);
186
+    };
187
+    SingleEventDef.prototype.isAllDay = function () {
188
+        return this.dateProfile.isAllDay();
189
+    };
190
+    SingleEventDef.prototype.clone = function () {
191
+        var def = _super.prototype.clone.call(this);
192
+        def.dateProfile = this.dateProfile;
193
+        return def;
194
+    };
195
+    SingleEventDef.prototype.rezone = function () {
196
+        var calendar = this.source.calendar;
197
+        var dateProfile = this.dateProfile;
198
+        this.dateProfile = new EventDateProfile_1.default(calendar.moment(dateProfile.start), dateProfile.end ? calendar.moment(dateProfile.end) : null, calendar);
199
+    };
200
+    /*
201
+    NOTE: if super-method fails, should still attempt to apply
202
+    */
203
+    SingleEventDef.prototype.applyManualStandardProps = function (rawProps) {
204
+        var superSuccess = _super.prototype.applyManualStandardProps.call(this, rawProps);
205
+        var dateProfile = EventDateProfile_1.default.parse(rawProps, this.source); // returns null on failure
206
+        if (dateProfile) {
207
+            this.dateProfile = dateProfile;
208
+            // make sure `date` shows up in the legacy event objects as-is
209
+            if (rawProps.date != null) {
210
+                this.miscProps.date = rawProps.date;
211
+            }
212
+            return superSuccess;
213
+        }
214
+        else {
215
+            return false;
216
+        }
217
+    };
218
+    return SingleEventDef;
219
+}(EventDef_1.default));
220
+exports.default = SingleEventDef;
221
+// Parsing
222
+// ---------------------------------------------------------------------------------------------------------------------
223
+SingleEventDef.defineStandardProps({
224
+    start: false,
225
+    date: false,
226
+    end: false,
227
+    allDay: false
228
+});
229
+
230
+
231
+/***/ }),
232
+/* 10 */,
233
+/* 11 */
234
 /***/ (function(module, exports, __webpack_require__) {
235
 
236
 Object.defineProperty(exports, "__esModule", { value: true });
237
@@ -1287,7 +1355,7 @@
238
     var mom = makeMoment(arguments, true);
239
     // Force it into UTC because makeMoment doesn't guarantee it
240
     // (if given a pre-existing moment for example)
241
-    if (mom.hasTime()) {
242
+    if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
243
         mom.utc();
244
     }
245
     return mom;
246
@@ -1314,7 +1382,7 @@
247
     if (moment.isMoment(input) || util_1.isNativeDate(input) || input === undefined) {
248
         mom = moment.apply(null, args);
249
     }
250
-    else {
251
+    else { // "parsing" is required
252
         isAmbigTime = false;
253
         isAmbigZone = false;
254
         if (isSingleString) {
255
@@ -1345,7 +1413,7 @@
256
             mom._ambigTime = true;
257
             mom._ambigZone = true; // ambiguous time always means ambiguous zone
258
         }
259
-        else if (parseZone) {
260
+        else if (parseZone) { // let's record the inputted zone somehow
261
             if (isAmbigZone) {
262
                 mom._ambigZone = true;
263
             }
264
@@ -1363,7 +1431,7 @@
265
 // `weeks` is an alias for `week`
266
 newMomentProto.week = newMomentProto.weeks = function (input) {
267
     var weekCalc = this._locale._fullCalendar_weekCalc;
268
-    if (input == null && typeof weekCalc === 'function') {
269
+    if (input == null && typeof weekCalc === 'function') { // custom function only works for getter
270
         return weekCalc(this);
271
     }
272
     else if (weekCalc === 'ISO') {
273
@@ -1386,7 +1454,7 @@
274
     if (!this._fullCalendar) {
275
         return oldMomentProto.time.apply(this, arguments);
276
     }
277
-    if (time == null) {
278
+    if (time == null) { // getter
279
         return moment.duration({
280
             hours: this.hours(),
281
             minutes: this.minutes(),
282
@@ -1394,7 +1462,7 @@
283
             milliseconds: this.milliseconds()
284
         });
285
     }
286
-    else {
287
+    else { // setter
288
         this._ambigTime = false; // mark that the moment now has a time
289
         if (!moment.isDuration(time) && !moment.isMoment(time)) {
290
             time = moment.duration(time);
291
@@ -1481,7 +1549,7 @@
292
 };
293
 // implicitly marks a zone (will probably get called upon .utc() and .local())
294
 newMomentProto.utcOffset = function (tzo) {
295
-    if (tzo != null) {
296
+    if (tzo != null) { // setter
297
         // these assignments needs to happen before the original zone method is called.
298
         // I forget why, something to do with a browser crash.
299
         this._ambigTime = false;
300
@@ -1492,7 +1560,35 @@
301
 
302
 
303
 /***/ }),
304
-/* 11 */
305
+/* 12 */
306
+/***/ (function(module, exports) {
307
+
308
+Object.defineProperty(exports, "__esModule", { value: true });
309
+/*
310
+Meant to be immutable
311
+*/
312
+var ComponentFootprint = /** @class */ (function () {
313
+    function ComponentFootprint(unzonedRange, isAllDay) {
314
+        this.isAllDay = false; // component can choose to ignore this
315
+        this.unzonedRange = unzonedRange;
316
+        this.isAllDay = isAllDay;
317
+    }
318
+    /*
319
+    Only works for non-open-ended ranges.
320
+    */
321
+    ComponentFootprint.prototype.toLegacy = function (calendar) {
322
+        return {
323
+            start: calendar.msToMoment(this.unzonedRange.startMs, this.isAllDay),
324
+            end: calendar.msToMoment(this.unzonedRange.endMs, this.isAllDay)
325
+        };
326
+    };
327
+    return ComponentFootprint;
328
+}());
329
+exports.default = ComponentFootprint;
330
+
331
+
332
+/***/ }),
333
+/* 13 */
334
 /***/ (function(module, exports, __webpack_require__) {
335
 
336
 /*
337
@@ -1511,7 +1607,7 @@
338
 Object.defineProperty(exports, "__esModule", { value: true });
339
 var tslib_1 = __webpack_require__(2);
340
 var $ = __webpack_require__(3);
341
-var Mixin_1 = __webpack_require__(14);
342
+var Mixin_1 = __webpack_require__(15);
343
 var EmitterMixin = /** @class */ (function (_super) {
344
     tslib_1.__extends(EmitterMixin, _super);
345
     function EmitterMixin() {
346
@@ -1573,103 +1669,28 @@
347
 
348
 
349
 /***/ }),
350
-/* 12 */
351
+/* 14 */
352
 /***/ (function(module, exports) {
353
 
354
 Object.defineProperty(exports, "__esModule", { value: true });
355
-/*
356
-Meant to be immutable
357
-*/
358
-var ComponentFootprint = /** @class */ (function () {
359
-    function ComponentFootprint(unzonedRange, isAllDay) {
360
-        this.isAllDay = false; // component can choose to ignore this
361
-        this.unzonedRange = unzonedRange;
362
-        this.isAllDay = isAllDay;
363
-    }
364
-    /*
365
-    Only works for non-open-ended ranges.
366
-    */
367
-    ComponentFootprint.prototype.toLegacy = function (calendar) {
368
-        return {
369
-            start: calendar.msToMoment(this.unzonedRange.startMs, this.isAllDay),
370
-            end: calendar.msToMoment(this.unzonedRange.endMs, this.isAllDay)
371
-        };
372
-    };
373
-    return ComponentFootprint;
374
-}());
375
-exports.default = ComponentFootprint;
376
-
377
-
378
-/***/ }),
379
-/* 13 */
380
-/***/ (function(module, exports, __webpack_require__) {
381
-
382
-Object.defineProperty(exports, "__esModule", { value: true });
383
-var tslib_1 = __webpack_require__(2);
384
-var EventDef_1 = __webpack_require__(34);
385
-var EventInstance_1 = __webpack_require__(209);
386
-var EventDateProfile_1 = __webpack_require__(17);
387
-var SingleEventDef = /** @class */ (function (_super) {
388
-    tslib_1.__extends(SingleEventDef, _super);
389
-    function SingleEventDef() {
390
-        return _super !== null && _super.apply(this, arguments) || this;
391
+var Interaction = /** @class */ (function () {
392
+    function Interaction(component) {
393
+        this.view = component._getView();
394
+        this.component = component;
395
     }
396
-    /*
397
-    Will receive start/end params, but will be ignored.
398
-    */
399
-    SingleEventDef.prototype.buildInstances = function () {
400
-        return [this.buildInstance()];
401
-    };
402
-    SingleEventDef.prototype.buildInstance = function () {
403
-        return new EventInstance_1.default(this, // definition
404
-        this.dateProfile);
405
-    };
406
-    SingleEventDef.prototype.isAllDay = function () {
407
-        return this.dateProfile.isAllDay();
408
-    };
409
-    SingleEventDef.prototype.clone = function () {
410
-        var def = _super.prototype.clone.call(this);
411
-        def.dateProfile = this.dateProfile;
412
-        return def;
413
-    };
414
-    SingleEventDef.prototype.rezone = function () {
415
-        var calendar = this.source.calendar;
416
-        var dateProfile = this.dateProfile;
417
-        this.dateProfile = new EventDateProfile_1.default(calendar.moment(dateProfile.start), dateProfile.end ? calendar.moment(dateProfile.end) : null, calendar);
418
+    Interaction.prototype.opt = function (name) {
419
+        return this.view.opt(name);
420
     };
421
-    /*
422
-    NOTE: if super-method fails, should still attempt to apply
423
-    */
424
-    SingleEventDef.prototype.applyManualStandardProps = function (rawProps) {
425
-        var superSuccess = _super.prototype.applyManualStandardProps.call(this, rawProps);
426
-        var dateProfile = EventDateProfile_1.default.parse(rawProps, this.source); // returns null on failure
427
-        if (dateProfile) {
428
-            this.dateProfile = dateProfile;
429
-            // make sure `date` shows up in the legacy event objects as-is
430
-            if (rawProps.date != null) {
431
-                this.miscProps.date = rawProps.date;
432
-            }
433
-            return superSuccess;
434
-        }
435
-        else {
436
-            return false;
437
-        }
438
+    Interaction.prototype.end = function () {
439
+        // subclasses can implement
440
     };
441
-    return SingleEventDef;
442
-}(EventDef_1.default));
443
-exports.default = SingleEventDef;
444
-// Parsing
445
-// ---------------------------------------------------------------------------------------------------------------------
446
-SingleEventDef.defineStandardProps({
447
-    start: false,
448
-    date: false,
449
-    end: false,
450
-    allDay: false
451
-});
452
+    return Interaction;
453
+}());
454
+exports.default = Interaction;
455
 
456
 
457
 /***/ }),
458
-/* 14 */
459
+/* 15 */
460
 /***/ (function(module, exports) {
461
 
462
 Object.defineProperty(exports, "__esModule", { value: true });
463
@@ -1679,7 +1700,7 @@
464
     Mixin.mixInto = function (destClass) {
465
         var _this = this;
466
         Object.getOwnPropertyNames(this.prototype).forEach(function (name) {
467
-            if (!destClass.prototype[name]) {
468
+            if (!destClass.prototype[name]) { // if destination class doesn't already define it
469
                 destClass.prototype[name] = _this.prototype[name];
470
             }
471
         });
472
@@ -1700,175 +1721,10 @@
473
 
474
 
475
 /***/ }),
476
-/* 15 */
477
-/***/ (function(module, exports) {
478
-
479
-Object.defineProperty(exports, "__esModule", { value: true });
480
-var Interaction = /** @class */ (function () {
481
-    function Interaction(component) {
482
-        this.view = component._getView();
483
-        this.component = component;
484
-    }
485
-    Interaction.prototype.opt = function (name) {
486
-        return this.view.opt(name);
487
-    };
488
-    Interaction.prototype.end = function () {
489
-        // subclasses can implement
490
-    };
491
-    return Interaction;
492
-}());
493
-exports.default = Interaction;
494
-
495
-
496
-/***/ }),
497
 /* 16 */
498
 /***/ (function(module, exports, __webpack_require__) {
499
 
500
 Object.defineProperty(exports, "__esModule", { value: true });
501
-exports.version = '3.9.0';
502
-// When introducing internal API incompatibilities (where fullcalendar plugins would break),
503
-// the minor version of the calendar should be upped (ex: 2.7.2 -> 2.8.0)
504
-// and the below integer should be incremented.
505
-exports.internalApiVersion = 12;
506
-var util_1 = __webpack_require__(4);
507
-exports.applyAll = util_1.applyAll;
508
-exports.debounce = util_1.debounce;
509
-exports.isInt = util_1.isInt;
510
-exports.htmlEscape = util_1.htmlEscape;
511
-exports.cssToStr = util_1.cssToStr;
512
-exports.proxy = util_1.proxy;
513
-exports.capitaliseFirstLetter = util_1.capitaliseFirstLetter;
514
-exports.getOuterRect = util_1.getOuterRect;
515
-exports.getClientRect = util_1.getClientRect;
516
-exports.getContentRect = util_1.getContentRect;
517
-exports.getScrollbarWidths = util_1.getScrollbarWidths;
518
-exports.preventDefault = util_1.preventDefault;
519
-exports.parseFieldSpecs = util_1.parseFieldSpecs;
520
-exports.compareByFieldSpecs = util_1.compareByFieldSpecs;
521
-exports.compareByFieldSpec = util_1.compareByFieldSpec;
522
-exports.flexibleCompare = util_1.flexibleCompare;
523
-exports.computeGreatestUnit = util_1.computeGreatestUnit;
524
-exports.divideRangeByDuration = util_1.divideRangeByDuration;
525
-exports.divideDurationByDuration = util_1.divideDurationByDuration;
526
-exports.multiplyDuration = util_1.multiplyDuration;
527
-exports.durationHasTime = util_1.durationHasTime;
528
-exports.log = util_1.log;
529
-exports.warn = util_1.warn;
530
-exports.removeExact = util_1.removeExact;
531
-exports.intersectRects = util_1.intersectRects;
532
-var date_formatting_1 = __webpack_require__(47);
533
-exports.formatDate = date_formatting_1.formatDate;
534
-exports.formatRange = date_formatting_1.formatRange;
535
-exports.queryMostGranularFormatUnit = date_formatting_1.queryMostGranularFormatUnit;
536
-var locale_1 = __webpack_require__(31);
537
-exports.datepickerLocale = locale_1.datepickerLocale;
538
-exports.locale = locale_1.locale;
539
-var moment_ext_1 = __webpack_require__(10);
540
-exports.moment = moment_ext_1.default;
541
-var EmitterMixin_1 = __webpack_require__(11);
542
-exports.EmitterMixin = EmitterMixin_1.default;
543
-var ListenerMixin_1 = __webpack_require__(7);
544
-exports.ListenerMixin = ListenerMixin_1.default;
545
-var Model_1 = __webpack_require__(48);
546
-exports.Model = Model_1.default;
547
-var Constraints_1 = __webpack_require__(207);
548
-exports.Constraints = Constraints_1.default;
549
-var UnzonedRange_1 = __webpack_require__(5);
550
-exports.UnzonedRange = UnzonedRange_1.default;
551
-var ComponentFootprint_1 = __webpack_require__(12);
552
-exports.ComponentFootprint = ComponentFootprint_1.default;
553
-var BusinessHourGenerator_1 = __webpack_require__(212);
554
-exports.BusinessHourGenerator = BusinessHourGenerator_1.default;
555
-var EventDef_1 = __webpack_require__(34);
556
-exports.EventDef = EventDef_1.default;
557
-var EventDefMutation_1 = __webpack_require__(37);
558
-exports.EventDefMutation = EventDefMutation_1.default;
559
-var EventSourceParser_1 = __webpack_require__(38);
560
-exports.EventSourceParser = EventSourceParser_1.default;
561
-var EventSource_1 = __webpack_require__(6);
562
-exports.EventSource = EventSource_1.default;
563
-var ThemeRegistry_1 = __webpack_require__(51);
564
-exports.defineThemeSystem = ThemeRegistry_1.defineThemeSystem;
565
-var EventInstanceGroup_1 = __webpack_require__(18);
566
-exports.EventInstanceGroup = EventInstanceGroup_1.default;
567
-var ArrayEventSource_1 = __webpack_require__(52);
568
-exports.ArrayEventSource = ArrayEventSource_1.default;
569
-var FuncEventSource_1 = __webpack_require__(215);
570
-exports.FuncEventSource = FuncEventSource_1.default;
571
-var JsonFeedEventSource_1 = __webpack_require__(216);
572
-exports.JsonFeedEventSource = JsonFeedEventSource_1.default;
573
-var EventFootprint_1 = __webpack_require__(36);
574
-exports.EventFootprint = EventFootprint_1.default;
575
-var Class_1 = __webpack_require__(33);
576
-exports.Class = Class_1.default;
577
-var Mixin_1 = __webpack_require__(14);
578
-exports.Mixin = Mixin_1.default;
579
-var CoordCache_1 = __webpack_require__(53);
580
-exports.CoordCache = CoordCache_1.default;
581
-var DragListener_1 = __webpack_require__(54);
582
-exports.DragListener = DragListener_1.default;
583
-var Promise_1 = __webpack_require__(20);
584
-exports.Promise = Promise_1.default;
585
-var TaskQueue_1 = __webpack_require__(217);
586
-exports.TaskQueue = TaskQueue_1.default;
587
-var RenderQueue_1 = __webpack_require__(218);
588
-exports.RenderQueue = RenderQueue_1.default;
589
-var Scroller_1 = __webpack_require__(39);
590
-exports.Scroller = Scroller_1.default;
591
-var Theme_1 = __webpack_require__(19);
592
-exports.Theme = Theme_1.default;
593
-var DateComponent_1 = __webpack_require__(219);
594
-exports.DateComponent = DateComponent_1.default;
595
-var InteractiveDateComponent_1 = __webpack_require__(40);
596
-exports.InteractiveDateComponent = InteractiveDateComponent_1.default;
597
-var Calendar_1 = __webpack_require__(220);
598
-exports.Calendar = Calendar_1.default;
599
-var View_1 = __webpack_require__(41);
600
-exports.View = View_1.default;
601
-var ViewRegistry_1 = __webpack_require__(22);
602
-exports.defineView = ViewRegistry_1.defineView;
603
-exports.getViewConfig = ViewRegistry_1.getViewConfig;
604
-var DayTableMixin_1 = __webpack_require__(55);
605
-exports.DayTableMixin = DayTableMixin_1.default;
606
-var BusinessHourRenderer_1 = __webpack_require__(56);
607
-exports.BusinessHourRenderer = BusinessHourRenderer_1.default;
608
-var EventRenderer_1 = __webpack_require__(42);
609
-exports.EventRenderer = EventRenderer_1.default;
610
-var FillRenderer_1 = __webpack_require__(57);
611
-exports.FillRenderer = FillRenderer_1.default;
612
-var HelperRenderer_1 = __webpack_require__(58);
613
-exports.HelperRenderer = HelperRenderer_1.default;
614
-var ExternalDropping_1 = __webpack_require__(222);
615
-exports.ExternalDropping = ExternalDropping_1.default;
616
-var EventResizing_1 = __webpack_require__(223);
617
-exports.EventResizing = EventResizing_1.default;
618
-var EventPointing_1 = __webpack_require__(59);
619
-exports.EventPointing = EventPointing_1.default;
620
-var EventDragging_1 = __webpack_require__(224);
621
-exports.EventDragging = EventDragging_1.default;
622
-var DateSelecting_1 = __webpack_require__(225);
623
-exports.DateSelecting = DateSelecting_1.default;
624
-var StandardInteractionsMixin_1 = __webpack_require__(60);
625
-exports.StandardInteractionsMixin = StandardInteractionsMixin_1.default;
626
-var AgendaView_1 = __webpack_require__(226);
627
-exports.AgendaView = AgendaView_1.default;
628
-var TimeGrid_1 = __webpack_require__(227);
629
-exports.TimeGrid = TimeGrid_1.default;
630
-var DayGrid_1 = __webpack_require__(61);
631
-exports.DayGrid = DayGrid_1.default;
632
-var BasicView_1 = __webpack_require__(62);
633
-exports.BasicView = BasicView_1.default;
634
-var MonthView_1 = __webpack_require__(229);
635
-exports.MonthView = MonthView_1.default;
636
-var ListView_1 = __webpack_require__(230);
637
-exports.ListView = ListView_1.default;
638
-
639
-
640
-/***/ }),
641
-/* 17 */
642
-/***/ (function(module, exports, __webpack_require__) {
643
-
644
-Object.defineProperty(exports, "__esModule", { value: true });
645
 var UnzonedRange_1 = __webpack_require__(5);
646
 /*
647
 Meant to be immutable
648
@@ -1896,9 +1752,6 @@
649
         if (!start.isValid()) {
650
             return false;
651
         }
652
-        if (end && (!end.isValid() || !end.isAfter(start))) {
653
-            end = null;
654
-        }
655
         if (forcedAllDay == null) {
656
             forcedAllDay = source.allDayDefault;
657
             if (forcedAllDay == null) {
658
@@ -1919,6 +1772,9 @@
659
                 end.time(0);
660
             }
661
         }
662
+        if (end && (!end.isValid() || !end.isAfter(start))) {
663
+            end = null;
664
+        }
665
         if (!end && forceEventDuration) {
666
             end = calendar.getDefaultEventEnd(!start.hasTime(), start);
667
         }
668
@@ -1953,139 +1809,517 @@
669
 
670
 
671
 /***/ }),
672
-/* 18 */
673
+/* 17 */
674
 /***/ (function(module, exports, __webpack_require__) {
675
 
676
 Object.defineProperty(exports, "__esModule", { value: true });
677
-var UnzonedRange_1 = __webpack_require__(5);
678
-var util_1 = __webpack_require__(35);
679
-var EventRange_1 = __webpack_require__(211);
680
-/*
681
-It's expected that there will be at least one EventInstance,
682
-OR that an explicitEventDef is assigned.
683
+var tslib_1 = __webpack_require__(2);
684
+var util_1 = __webpack_require__(4);
685
+var DragListener_1 = __webpack_require__(59);
686
+/* Tracks mouse movements over a component and raises events about which hit the mouse is over.
687
+------------------------------------------------------------------------------------------------------------------------
688
+options:
689
+- subjectEl
690
+- subjectCenter
691
 */
692
-var EventInstanceGroup = /** @class */ (function () {
693
-    function EventInstanceGroup(eventInstances) {
694
-        this.eventInstances = eventInstances || [];
695
+var HitDragListener = /** @class */ (function (_super) {
696
+    tslib_1.__extends(HitDragListener, _super);
697
+    function HitDragListener(component, options) {
698
+        var _this = _super.call(this, options) || this;
699
+        _this.component = component;
700
+        return _this;
701
     }
702
-    EventInstanceGroup.prototype.getAllEventRanges = function (constraintRange) {
703
-        if (constraintRange) {
704
-            return this.sliceNormalRenderRanges(constraintRange);
705
+    // Called when drag listening starts (but a real drag has not necessarily began).
706
+    // ev might be undefined if dragging was started manually.
707
+    HitDragListener.prototype.handleInteractionStart = function (ev) {
708
+        var subjectEl = this.subjectEl;
709
+        var subjectRect;
710
+        var origPoint;
711
+        var point;
712
+        this.component.hitsNeeded();
713
+        this.computeScrollBounds(); // for autoscroll
714
+        if (ev) {
715
+            origPoint = { left: util_1.getEvX(ev), top: util_1.getEvY(ev) };
716
+            point = origPoint;
717
+            // constrain the point to bounds of the element being dragged
718
+            if (subjectEl) {
719
+                subjectRect = util_1.getOuterRect(subjectEl); // used for centering as well
720
+                point = util_1.constrainPoint(point, subjectRect);
721
+            }
722
+            this.origHit = this.queryHit(point.left, point.top);
723
+            // treat the center of the subject as the collision point?
724
+            if (subjectEl && this.options.subjectCenter) {
725
+                // only consider the area the subject overlaps the hit. best for large subjects.
726
+                // TODO: skip this if hit didn't supply left/right/top/bottom
727
+                if (this.origHit) {
728
+                    subjectRect = util_1.intersectRects(this.origHit, subjectRect) ||
729
+                        subjectRect; // in case there is no intersection
730
+                }
731
+                point = util_1.getRectCenter(subjectRect);
732
+            }
733
+            this.coordAdjust = util_1.diffPoints(point, origPoint); // point - origPoint
734
         }
735
         else {
736
-            return this.eventInstances.map(util_1.eventInstanceToEventRange);
737
+            this.origHit = null;
738
+            this.coordAdjust = null;
739
         }
740
+        // call the super-method. do it after origHit has been computed
741
+        _super.prototype.handleInteractionStart.call(this, ev);
742
     };
743
-    EventInstanceGroup.prototype.sliceRenderRanges = function (constraintRange) {
744
-        if (this.isInverse()) {
745
-            return this.sliceInverseRenderRanges(constraintRange);
746
-        }
747
-        else {
748
-            return this.sliceNormalRenderRanges(constraintRange);
749
+    // Called when the actual drag has started
750
+    HitDragListener.prototype.handleDragStart = function (ev) {
751
+        var hit;
752
+        _super.prototype.handleDragStart.call(this, ev);
753
+        // might be different from this.origHit if the min-distance is large
754
+        hit = this.queryHit(util_1.getEvX(ev), util_1.getEvY(ev));
755
+        // report the initial hit the mouse is over
756
+        // especially important if no min-distance and drag starts immediately
757
+        if (hit) {
758
+            this.handleHitOver(hit);
759
         }
760
     };
761
-    EventInstanceGroup.prototype.sliceNormalRenderRanges = function (constraintRange) {
762
-        var eventInstances = this.eventInstances;
763
-        var i;
764
-        var eventInstance;
765
-        var slicedRange;
766
-        var slicedEventRanges = [];
767
-        for (i = 0; i < eventInstances.length; i++) {
768
-            eventInstance = eventInstances[i];
769
-            slicedRange = eventInstance.dateProfile.unzonedRange.intersect(constraintRange);
770
-            if (slicedRange) {
771
-                slicedEventRanges.push(new EventRange_1.default(slicedRange, eventInstance.def, eventInstance));
772
+    // Called when the drag moves
773
+    HitDragListener.prototype.handleDrag = function (dx, dy, ev) {
774
+        var hit;
775
+        _super.prototype.handleDrag.call(this, dx, dy, ev);
776
+        hit = this.queryHit(util_1.getEvX(ev), util_1.getEvY(ev));
777
+        if (!isHitsEqual(hit, this.hit)) { // a different hit than before?
778
+            if (this.hit) {
779
+                this.handleHitOut();
780
+            }
781
+            if (hit) {
782
+                this.handleHitOver(hit);
783
             }
784
         }
785
-        return slicedEventRanges;
786
-    };
787
-    EventInstanceGroup.prototype.sliceInverseRenderRanges = function (constraintRange) {
788
-        var unzonedRanges = this.eventInstances.map(util_1.eventInstanceToUnzonedRange);
789
-        var ownerDef = this.getEventDef();
790
-        unzonedRanges = UnzonedRange_1.default.invertRanges(unzonedRanges, constraintRange);
791
-        return unzonedRanges.map(function (unzonedRange) {
792
-            return new EventRange_1.default(unzonedRange, ownerDef); // don't give an EventInstance
793
-        });
794
-    };
795
-    EventInstanceGroup.prototype.isInverse = function () {
796
-        return this.getEventDef().hasInverseRendering();
797
     };
798
-    EventInstanceGroup.prototype.getEventDef = function () {
799
-        return this.explicitEventDef || this.eventInstances[0].def;
800
+    // Called when dragging has been stopped
801
+    HitDragListener.prototype.handleDragEnd = function (ev) {
802
+        this.handleHitDone();
803
+        _super.prototype.handleDragEnd.call(this, ev);
804
     };
805
-    return EventInstanceGroup;
806
-}());
807
-exports.default = EventInstanceGroup;
808
-
809
-
810
-/***/ }),
811
-/* 19 */
812
-/***/ (function(module, exports, __webpack_require__) {
813
-
814
-Object.defineProperty(exports, "__esModule", { value: true });
815
-var $ = __webpack_require__(3);
816
-var Theme = /** @class */ (function () {
817
-    function Theme(optionsManager) {
818
-        this.optionsManager = optionsManager;
819
-        this.processIconOverride();
820
-    }
821
-    Theme.prototype.processIconOverride = function () {
822
-        if (this.iconOverrideOption) {
823
-            this.setIconOverride(this.optionsManager.get(this.iconOverrideOption));
824
-        }
825
+    // Called when a the mouse has just moved over a new hit
826
+    HitDragListener.prototype.handleHitOver = function (hit) {
827
+        var isOrig = isHitsEqual(hit, this.origHit);
828
+        this.hit = hit;
829
+        this.trigger('hitOver', this.hit, isOrig, this.origHit);
830
     };
831
-    Theme.prototype.setIconOverride = function (iconOverrideHash) {
832
-        var iconClassesCopy;
833
-        var buttonName;
834
-        if ($.isPlainObject(iconOverrideHash)) {
835
-            iconClassesCopy = $.extend({}, this.iconClasses);
836
-            for (buttonName in iconOverrideHash) {
837
-                iconClassesCopy[buttonName] = this.applyIconOverridePrefix(iconOverrideHash[buttonName]);
838
-            }
839
-            this.iconClasses = iconClassesCopy;
840
-        }
841
-        else if (iconOverrideHash === false) {
842
-            this.iconClasses = {};
843
+    // Called when the mouse has just moved out of a hit
844
+    HitDragListener.prototype.handleHitOut = function () {
845
+        if (this.hit) {
846
+            this.trigger('hitOut', this.hit);
847
+            this.handleHitDone();
848
+            this.hit = null;
849
         }
850
     };
851
-    Theme.prototype.applyIconOverridePrefix = function (className) {
852
-        var prefix = this.iconOverridePrefix;
853
-        if (prefix && className.indexOf(prefix) !== 0) {
854
-            className = prefix + className;
855
+    // Called after a hitOut. Also called before a dragStop
856
+    HitDragListener.prototype.handleHitDone = function () {
857
+        if (this.hit) {
858
+            this.trigger('hitDone', this.hit);
859
         }
860
-        return className;
861
     };
862
-    Theme.prototype.getClass = function (key) {
863
-        return this.classes[key] || '';
864
+    // Called when the interaction ends, whether there was a real drag or not
865
+    HitDragListener.prototype.handleInteractionEnd = function (ev, isCancelled) {
866
+        _super.prototype.handleInteractionEnd.call(this, ev, isCancelled);
867
+        this.origHit = null;
868
+        this.hit = null;
869
+        this.component.hitsNotNeeded();
870
     };
871
-    Theme.prototype.getIconClass = function (buttonName) {
872
-        var className = this.iconClasses[buttonName];
873
-        if (className) {
874
-            return this.baseIconClass + ' ' + className;
875
+    // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
876
+    HitDragListener.prototype.handleScrollEnd = function () {
877
+        _super.prototype.handleScrollEnd.call(this);
878
+        // hits' absolute positions will be in new places after a user's scroll.
879
+        // HACK for recomputing.
880
+        if (this.isDragging) {
881
+            this.component.releaseHits();
882
+            this.component.prepareHits();
883
         }
884
-        return '';
885
     };
886
-    Theme.prototype.getCustomButtonIconClass = function (customButtonProps) {
887
-        var className;
888
-        if (this.iconOverrideCustomButtonOption) {
889
-            className = customButtonProps[this.iconOverrideCustomButtonOption];
890
-            if (className) {
891
-                return this.baseIconClass + ' ' + this.applyIconOverridePrefix(className);
892
+    // Gets the hit underneath the coordinates for the given mouse event
893
+    HitDragListener.prototype.queryHit = function (left, top) {
894
+        if (this.coordAdjust) {
895
+            left += this.coordAdjust.left;
896
+            top += this.coordAdjust.top;
897
+        }
898
+        return this.component.queryHit(left, top);
899
+    };
900
+    return HitDragListener;
901
+}(DragListener_1.default));
902
+exports.default = HitDragListener;
903
+// Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component.
904
+// Two null values will be considered equal, as two "out of the component" states are the same.
905
+function isHitsEqual(hit0, hit1) {
906
+    if (!hit0 && !hit1) {
907
+        return true;
908
+    }
909
+    if (hit0 && hit1) {
910
+        return hit0.component === hit1.component &&
911
+            isHitPropsWithin(hit0, hit1) &&
912
+            isHitPropsWithin(hit1, hit0); // ensures all props are identical
913
+    }
914
+    return false;
915
+}
916
+// Returns true if all of subHit's non-standard properties are within superHit
917
+function isHitPropsWithin(subHit, superHit) {
918
+    for (var propName in subHit) {
919
+        if (!/^(component|left|right|top|bottom)$/.test(propName)) {
920
+            if (subHit[propName] !== superHit[propName]) {
921
+                return false;
922
             }
923
         }
924
-        return '';
925
+    }
926
+    return true;
927
+}
928
+
929
+
930
+/***/ }),
931
+/* 18 */
932
+/***/ (function(module, exports, __webpack_require__) {
933
+
934
+Object.defineProperty(exports, "__esModule", { value: true });
935
+exports.version = '3.10.2';
936
+// When introducing internal API incompatibilities (where fullcalendar plugins would break),
937
+// the minor version of the calendar should be upped (ex: 2.7.2 -> 2.8.0)
938
+// and the below integer should be incremented.
939
+exports.internalApiVersion = 12;
940
+var util_1 = __webpack_require__(4);
941
+exports.applyAll = util_1.applyAll;
942
+exports.debounce = util_1.debounce;
943
+exports.isInt = util_1.isInt;
944
+exports.htmlEscape = util_1.htmlEscape;
945
+exports.cssToStr = util_1.cssToStr;
946
+exports.proxy = util_1.proxy;
947
+exports.capitaliseFirstLetter = util_1.capitaliseFirstLetter;
948
+exports.getOuterRect = util_1.getOuterRect;
949
+exports.getClientRect = util_1.getClientRect;
950
+exports.getContentRect = util_1.getContentRect;
951
+exports.getScrollbarWidths = util_1.getScrollbarWidths;
952
+exports.preventDefault = util_1.preventDefault;
953
+exports.parseFieldSpecs = util_1.parseFieldSpecs;
954
+exports.compareByFieldSpecs = util_1.compareByFieldSpecs;
955
+exports.compareByFieldSpec = util_1.compareByFieldSpec;
956
+exports.flexibleCompare = util_1.flexibleCompare;
957
+exports.computeGreatestUnit = util_1.computeGreatestUnit;
958
+exports.divideRangeByDuration = util_1.divideRangeByDuration;
959
+exports.divideDurationByDuration = util_1.divideDurationByDuration;
960
+exports.multiplyDuration = util_1.multiplyDuration;
961
+exports.durationHasTime = util_1.durationHasTime;
962
+exports.log = util_1.log;
963
+exports.warn = util_1.warn;
964
+exports.removeExact = util_1.removeExact;
965
+exports.intersectRects = util_1.intersectRects;
966
+exports.allowSelection = util_1.allowSelection;
967
+exports.attrsToStr = util_1.attrsToStr;
968
+exports.compareNumbers = util_1.compareNumbers;
969
+exports.compensateScroll = util_1.compensateScroll;
970
+exports.computeDurationGreatestUnit = util_1.computeDurationGreatestUnit;
971
+exports.constrainPoint = util_1.constrainPoint;
972
+exports.copyOwnProps = util_1.copyOwnProps;
973
+exports.diffByUnit = util_1.diffByUnit;
974
+exports.diffDay = util_1.diffDay;
975
+exports.diffDayTime = util_1.diffDayTime;
976
+exports.diffPoints = util_1.diffPoints;
977
+exports.disableCursor = util_1.disableCursor;
978
+exports.distributeHeight = util_1.distributeHeight;
979
+exports.enableCursor = util_1.enableCursor;
980
+exports.firstDefined = util_1.firstDefined;
981
+exports.getEvIsTouch = util_1.getEvIsTouch;
982
+exports.getEvX = util_1.getEvX;
983
+exports.getEvY = util_1.getEvY;
984
+exports.getRectCenter = util_1.getRectCenter;
985
+exports.getScrollParent = util_1.getScrollParent;
986
+exports.hasOwnProp = util_1.hasOwnProp;
987
+exports.isArraysEqual = util_1.isArraysEqual;
988
+exports.isNativeDate = util_1.isNativeDate;
989
+exports.isPrimaryMouseButton = util_1.isPrimaryMouseButton;
990
+exports.isTimeString = util_1.isTimeString;
991
+exports.matchCellWidths = util_1.matchCellWidths;
992
+exports.mergeProps = util_1.mergeProps;
993
+exports.preventSelection = util_1.preventSelection;
994
+exports.removeMatching = util_1.removeMatching;
995
+exports.stripHtmlEntities = util_1.stripHtmlEntities;
996
+exports.subtractInnerElHeight = util_1.subtractInnerElHeight;
997
+exports.uncompensateScroll = util_1.uncompensateScroll;
998
+exports.undistributeHeight = util_1.undistributeHeight;
999
+exports.dayIDs = util_1.dayIDs;
1000
+exports.unitsDesc = util_1.unitsDesc;
1001
+var date_formatting_1 = __webpack_require__(49);
1002
+exports.formatDate = date_formatting_1.formatDate;
1003
+exports.formatRange = date_formatting_1.formatRange;
1004
+exports.queryMostGranularFormatUnit = date_formatting_1.queryMostGranularFormatUnit;
1005
+var locale_1 = __webpack_require__(32);
1006
+exports.datepickerLocale = locale_1.datepickerLocale;
1007
+exports.locale = locale_1.locale;
1008
+exports.getMomentLocaleData = locale_1.getMomentLocaleData;
1009
+exports.populateInstanceComputableOptions = locale_1.populateInstanceComputableOptions;
1010
+var util_2 = __webpack_require__(19);
1011
+exports.eventDefsToEventInstances = util_2.eventDefsToEventInstances;
1012
+exports.eventFootprintToComponentFootprint = util_2.eventFootprintToComponentFootprint;
1013
+exports.eventInstanceToEventRange = util_2.eventInstanceToEventRange;
1014
+exports.eventInstanceToUnzonedRange = util_2.eventInstanceToUnzonedRange;
1015
+exports.eventRangeToEventFootprint = util_2.eventRangeToEventFootprint;
1016
+var moment_ext_1 = __webpack_require__(11);
1017
+exports.moment = moment_ext_1.default;
1018
+var EmitterMixin_1 = __webpack_require__(13);
1019
+exports.EmitterMixin = EmitterMixin_1.default;
1020
+var ListenerMixin_1 = __webpack_require__(7);
1021
+exports.ListenerMixin = ListenerMixin_1.default;
1022
+var Model_1 = __webpack_require__(51);
1023
+exports.Model = Model_1.default;
1024
+var Constraints_1 = __webpack_require__(217);
1025
+exports.Constraints = Constraints_1.default;
1026
+var DateProfileGenerator_1 = __webpack_require__(55);
1027
+exports.DateProfileGenerator = DateProfileGenerator_1.default;
1028
+var UnzonedRange_1 = __webpack_require__(5);
1029
+exports.UnzonedRange = UnzonedRange_1.default;
1030
+var ComponentFootprint_1 = __webpack_require__(12);
1031
+exports.ComponentFootprint = ComponentFootprint_1.default;
1032
+var BusinessHourGenerator_1 = __webpack_require__(218);
1033
+exports.BusinessHourGenerator = BusinessHourGenerator_1.default;
1034
+var EventPeriod_1 = __webpack_require__(219);
1035
+exports.EventPeriod = EventPeriod_1.default;
1036
+var EventManager_1 = __webpack_require__(220);
1037
+exports.EventManager = EventManager_1.default;
1038
+var EventDef_1 = __webpack_require__(37);
1039
+exports.EventDef = EventDef_1.default;
1040
+var EventDefMutation_1 = __webpack_require__(39);
1041
+exports.EventDefMutation = EventDefMutation_1.default;
1042
+var EventDefParser_1 = __webpack_require__(36);
1043
+exports.EventDefParser = EventDefParser_1.default;
1044
+var EventInstance_1 = __webpack_require__(53);
1045
+exports.EventInstance = EventInstance_1.default;
1046
+var EventRange_1 = __webpack_require__(50);
1047
+exports.EventRange = EventRange_1.default;
1048
+var RecurringEventDef_1 = __webpack_require__(54);
1049
+exports.RecurringEventDef = RecurringEventDef_1.default;
1050
+var SingleEventDef_1 = __webpack_require__(9);
1051
+exports.SingleEventDef = SingleEventDef_1.default;
1052
+var EventDefDateMutation_1 = __webpack_require__(40);
1053
+exports.EventDefDateMutation = EventDefDateMutation_1.default;
1054
+var EventDateProfile_1 = __webpack_require__(16);
1055
+exports.EventDateProfile = EventDateProfile_1.default;
1056
+var EventSourceParser_1 = __webpack_require__(38);
1057
+exports.EventSourceParser = EventSourceParser_1.default;
1058
+var EventSource_1 = __webpack_require__(6);
1059
+exports.EventSource = EventSource_1.default;
1060
+var ThemeRegistry_1 = __webpack_require__(57);
1061
+exports.defineThemeSystem = ThemeRegistry_1.defineThemeSystem;
1062
+exports.getThemeSystemClass = ThemeRegistry_1.getThemeSystemClass;
1063
+var EventInstanceGroup_1 = __webpack_require__(20);
1064
+exports.EventInstanceGroup = EventInstanceGroup_1.default;
1065
+var ArrayEventSource_1 = __webpack_require__(56);
1066
+exports.ArrayEventSource = ArrayEventSource_1.default;
1067
+var FuncEventSource_1 = __webpack_require__(223);
1068
+exports.FuncEventSource = FuncEventSource_1.default;
1069
+var JsonFeedEventSource_1 = __webpack_require__(224);
1070
+exports.JsonFeedEventSource = JsonFeedEventSource_1.default;
1071
+var EventFootprint_1 = __webpack_require__(34);
1072
+exports.EventFootprint = EventFootprint_1.default;
1073
+var Class_1 = __webpack_require__(35);
1074
+exports.Class = Class_1.default;
1075
+var Mixin_1 = __webpack_require__(15);
1076
+exports.Mixin = Mixin_1.default;
1077
+var CoordCache_1 = __webpack_require__(58);
1078
+exports.CoordCache = CoordCache_1.default;
1079
+var Iterator_1 = __webpack_require__(225);
1080
+exports.Iterator = Iterator_1.default;
1081
+var DragListener_1 = __webpack_require__(59);
1082
+exports.DragListener = DragListener_1.default;
1083
+var HitDragListener_1 = __webpack_require__(17);
1084
+exports.HitDragListener = HitDragListener_1.default;
1085
+var MouseFollower_1 = __webpack_require__(226);
1086
+exports.MouseFollower = MouseFollower_1.default;
1087
+var ParsableModelMixin_1 = __webpack_require__(52);
1088
+exports.ParsableModelMixin = ParsableModelMixin_1.default;
1089
+var Popover_1 = __webpack_require__(227);
1090
+exports.Popover = Popover_1.default;
1091
+var Promise_1 = __webpack_require__(21);
1092
+exports.Promise = Promise_1.default;
1093
+var TaskQueue_1 = __webpack_require__(228);
1094
+exports.TaskQueue = TaskQueue_1.default;
1095
+var RenderQueue_1 = __webpack_require__(229);
1096
+exports.RenderQueue = RenderQueue_1.default;
1097
+var Scroller_1 = __webpack_require__(41);
1098
+exports.Scroller = Scroller_1.default;
1099
+var Theme_1 = __webpack_require__(22);
1100
+exports.Theme = Theme_1.default;
1101
+var Component_1 = __webpack_require__(230);
1102
+exports.Component = Component_1.default;
1103
+var DateComponent_1 = __webpack_require__(231);
1104
+exports.DateComponent = DateComponent_1.default;
1105
+var InteractiveDateComponent_1 = __webpack_require__(42);
1106
+exports.InteractiveDateComponent = InteractiveDateComponent_1.default;
1107
+var Calendar_1 = __webpack_require__(232);
1108
+exports.Calendar = Calendar_1.default;
1109
+var View_1 = __webpack_require__(43);
1110
+exports.View = View_1.default;
1111
+var ViewRegistry_1 = __webpack_require__(24);
1112
+exports.defineView = ViewRegistry_1.defineView;
1113
+exports.getViewConfig = ViewRegistry_1.getViewConfig;
1114
+var DayTableMixin_1 = __webpack_require__(60);
1115
+exports.DayTableMixin = DayTableMixin_1.default;
1116
+var BusinessHourRenderer_1 = __webpack_require__(61);
1117
+exports.BusinessHourRenderer = BusinessHourRenderer_1.default;
1118
+var EventRenderer_1 = __webpack_require__(44);
1119
+exports.EventRenderer = EventRenderer_1.default;
1120
+var FillRenderer_1 = __webpack_require__(62);
1121
+exports.FillRenderer = FillRenderer_1.default;
1122
+var HelperRenderer_1 = __webpack_require__(63);
1123
+exports.HelperRenderer = HelperRenderer_1.default;
1124
+var ExternalDropping_1 = __webpack_require__(233);
1125
+exports.ExternalDropping = ExternalDropping_1.default;
1126
+var EventResizing_1 = __webpack_require__(234);
1127
+exports.EventResizing = EventResizing_1.default;
1128
+var EventPointing_1 = __webpack_require__(64);
1129
+exports.EventPointing = EventPointing_1.default;
1130
+var EventDragging_1 = __webpack_require__(235);
1131
+exports.EventDragging = EventDragging_1.default;
1132
+var DateSelecting_1 = __webpack_require__(236);
1133
+exports.DateSelecting = DateSelecting_1.default;
1134
+var DateClicking_1 = __webpack_require__(237);
1135
+exports.DateClicking = DateClicking_1.default;
1136
+var Interaction_1 = __webpack_require__(14);
1137
+exports.Interaction = Interaction_1.default;
1138
+var StandardInteractionsMixin_1 = __webpack_require__(65);
1139
+exports.StandardInteractionsMixin = StandardInteractionsMixin_1.default;
1140
+var AgendaView_1 = __webpack_require__(238);
1141
+exports.AgendaView = AgendaView_1.default;
1142
+var TimeGrid_1 = __webpack_require__(239);
1143
+exports.TimeGrid = TimeGrid_1.default;
1144
+var TimeGridEventRenderer_1 = __webpack_require__(240);
1145
+exports.TimeGridEventRenderer = TimeGridEventRenderer_1.default;
1146
+var TimeGridFillRenderer_1 = __webpack_require__(242);
1147
+exports.TimeGridFillRenderer = TimeGridFillRenderer_1.default;
1148
+var TimeGridHelperRenderer_1 = __webpack_require__(241);
1149
+exports.TimeGridHelperRenderer = TimeGridHelperRenderer_1.default;
1150
+var DayGrid_1 = __webpack_require__(66);
1151
+exports.DayGrid = DayGrid_1.default;
1152
+var DayGridEventRenderer_1 = __webpack_require__(243);
1153
+exports.DayGridEventRenderer = DayGridEventRenderer_1.default;
1154
+var DayGridFillRenderer_1 = __webpack_require__(245);
1155
+exports.DayGridFillRenderer = DayGridFillRenderer_1.default;
1156
+var DayGridHelperRenderer_1 = __webpack_require__(244);
1157
+exports.DayGridHelperRenderer = DayGridHelperRenderer_1.default;
1158
+var BasicView_1 = __webpack_require__(67);
1159
+exports.BasicView = BasicView_1.default;
1160
+var BasicViewDateProfileGenerator_1 = __webpack_require__(68);
1161
+exports.BasicViewDateProfileGenerator = BasicViewDateProfileGenerator_1.default;
1162
+var MonthView_1 = __webpack_require__(246);
1163
+exports.MonthView = MonthView_1.default;
1164
+var MonthViewDateProfileGenerator_1 = __webpack_require__(247);
1165
+exports.MonthViewDateProfileGenerator = MonthViewDateProfileGenerator_1.default;
1166
+var ListView_1 = __webpack_require__(248);
1167
+exports.ListView = ListView_1.default;
1168
+var ListEventPointing_1 = __webpack_require__(250);
1169
+exports.ListEventPointing = ListEventPointing_1.default;
1170
+var ListEventRenderer_1 = __webpack_require__(249);
1171
+exports.ListEventRenderer = ListEventRenderer_1.default;
1172
+
1173
+
1174
+/***/ }),
1175
+/* 19 */
1176
+/***/ (function(module, exports, __webpack_require__) {
1177
+
1178
+Object.defineProperty(exports, "__esModule", { value: true });
1179
+var EventRange_1 = __webpack_require__(50);
1180
+var EventFootprint_1 = __webpack_require__(34);
1181
+var ComponentFootprint_1 = __webpack_require__(12);
1182
+function eventDefsToEventInstances(eventDefs, unzonedRange) {
1183
+    var eventInstances = [];
1184
+    var i;
1185
+    for (i = 0; i < eventDefs.length; i++) {
1186
+        eventInstances.push.apply(eventInstances, // append
1187
+        eventDefs[i].buildInstances(unzonedRange));
1188
+    }
1189
+    return eventInstances;
1190
+}
1191
+exports.eventDefsToEventInstances = eventDefsToEventInstances;
1192
+function eventInstanceToEventRange(eventInstance) {
1193
+    return new EventRange_1.default(eventInstance.dateProfile.unzonedRange, eventInstance.def, eventInstance);
1194
+}
1195
+exports.eventInstanceToEventRange = eventInstanceToEventRange;
1196
+function eventRangeToEventFootprint(eventRange) {
1197
+    return new EventFootprint_1.default(new ComponentFootprint_1.default(eventRange.unzonedRange, eventRange.eventDef.isAllDay()), eventRange.eventDef, eventRange.eventInstance // might not exist
1198
+    );
1199
+}
1200
+exports.eventRangeToEventFootprint = eventRangeToEventFootprint;
1201
+function eventInstanceToUnzonedRange(eventInstance) {
1202
+    return eventInstance.dateProfile.unzonedRange;
1203
+}
1204
+exports.eventInstanceToUnzonedRange = eventInstanceToUnzonedRange;
1205
+function eventFootprintToComponentFootprint(eventFootprint) {
1206
+    return eventFootprint.componentFootprint;
1207
+}
1208
+exports.eventFootprintToComponentFootprint = eventFootprintToComponentFootprint;
1209
+
1210
+
1211
+/***/ }),
1212
+/* 20 */
1213
+/***/ (function(module, exports, __webpack_require__) {
1214
+
1215
+Object.defineProperty(exports, "__esModule", { value: true });
1216
+var UnzonedRange_1 = __webpack_require__(5);
1217
+var util_1 = __webpack_require__(19);
1218
+var EventRange_1 = __webpack_require__(50);
1219
+/*
1220
+It's expected that there will be at least one EventInstance,
1221
+OR that an explicitEventDef is assigned.
1222
+*/
1223
+var EventInstanceGroup = /** @class */ (function () {
1224
+    function EventInstanceGroup(eventInstances) {
1225
+        this.eventInstances = eventInstances || [];
1226
+    }
1227
+    EventInstanceGroup.prototype.getAllEventRanges = function (constraintRange) {
1228
+        if (constraintRange) {
1229
+            return this.sliceNormalRenderRanges(constraintRange);
1230
+        }
1231
+        else {
1232
+            return this.eventInstances.map(util_1.eventInstanceToEventRange);
1233
+        }
1234
     };
1235
-    return Theme;
1236
+    EventInstanceGroup.prototype.sliceRenderRanges = function (constraintRange) {
1237
+        if (this.isInverse()) {
1238
+            return this.sliceInverseRenderRanges(constraintRange);
1239
+        }
1240
+        else {
1241
+            return this.sliceNormalRenderRanges(constraintRange);
1242
+        }
1243
+    };
1244
+    EventInstanceGroup.prototype.sliceNormalRenderRanges = function (constraintRange) {
1245
+        var eventInstances = this.eventInstances;
1246
+        var i;
1247
+        var eventInstance;
1248
+        var slicedRange;
1249
+        var slicedEventRanges = [];
1250
+        for (i = 0; i < eventInstances.length; i++) {
1251
+            eventInstance = eventInstances[i];
1252
+            slicedRange = eventInstance.dateProfile.unzonedRange.intersect(constraintRange);
1253
+            if (slicedRange) {
1254
+                slicedEventRanges.push(new EventRange_1.default(slicedRange, eventInstance.def, eventInstance));
1255
+            }
1256
+        }
1257
+        return slicedEventRanges;
1258
+    };
1259
+    EventInstanceGroup.prototype.sliceInverseRenderRanges = function (constraintRange) {
1260
+        var unzonedRanges = this.eventInstances.map(util_1.eventInstanceToUnzonedRange);
1261
+        var ownerDef = this.getEventDef();
1262
+        unzonedRanges = UnzonedRange_1.default.invertRanges(unzonedRanges, constraintRange);
1263
+        return unzonedRanges.map(function (unzonedRange) {
1264
+            return new EventRange_1.default(unzonedRange, ownerDef); // don't give an EventInstance
1265
+        });
1266
+    };
1267
+    EventInstanceGroup.prototype.isInverse = function () {
1268
+        return this.getEventDef().hasInverseRendering();
1269
+    };
1270
+    EventInstanceGroup.prototype.getEventDef = function () {
1271
+        return this.explicitEventDef || this.eventInstances[0].def;
1272
+    };
1273
+    return EventInstanceGroup;
1274
 }());
1275
-exports.default = Theme;
1276
-Theme.prototype.classes = {};
1277
-Theme.prototype.iconClasses = {};
1278
-Theme.prototype.baseIconClass = '';
1279
-Theme.prototype.iconOverridePrefix = '';
1280
+exports.default = EventInstanceGroup;
1281
 
1282
 
1283
 /***/ }),
1284
-/* 20 */
1285
+/* 21 */
1286
 /***/ (function(module, exports, __webpack_require__) {
1287
 
1288
 Object.defineProperty(exports, "__esModule", { value: true });
1289
@@ -2138,30 +2372,96 @@
1290
 
1291
 
1292
 /***/ }),
1293
-/* 21 */
1294
+/* 22 */
1295
 /***/ (function(module, exports, __webpack_require__) {
1296
 
1297
 Object.defineProperty(exports, "__esModule", { value: true });
1298
 var $ = __webpack_require__(3);
1299
-var exportHooks = __webpack_require__(16);
1300
-var EmitterMixin_1 = __webpack_require__(11);
1301
-var ListenerMixin_1 = __webpack_require__(7);
1302
-exportHooks.touchMouseIgnoreWait = 500;
1303
-var globalEmitter = null;
1304
-var neededCount = 0;
1305
-/*
1306
-Listens to document and window-level user-interaction events, like touch events and mouse events,
1307
-and fires these events as-is to whoever is observing a GlobalEmitter.
1308
-Best when used as a singleton via GlobalEmitter.get()
1309
-
1310
-Normalizes mouse/touch events. For examples:
1311
-- ignores the the simulated mouse events that happen after a quick tap: mousemove+mousedown+mouseup+click
1312
-- compensates for various buggy scenarios where a touchend does not fire
1313
-*/
1314
-var GlobalEmitter = /** @class */ (function () {
1315
-    function GlobalEmitter() {
1316
-        this.isTouching = false;
1317
-        this.mouseIgnoreDepth = 0;
1318
+var Theme = /** @class */ (function () {
1319
+    function Theme(optionsManager) {
1320
+        this.optionsManager = optionsManager;
1321
+        this.processIconOverride();
1322
+    }
1323
+    Theme.prototype.processIconOverride = function () {
1324
+        if (this.iconOverrideOption) {
1325
+            this.setIconOverride(this.optionsManager.get(this.iconOverrideOption));
1326
+        }
1327
+    };
1328
+    Theme.prototype.setIconOverride = function (iconOverrideHash) {
1329
+        var iconClassesCopy;
1330
+        var buttonName;
1331
+        if ($.isPlainObject(iconOverrideHash)) {
1332
+            iconClassesCopy = $.extend({}, this.iconClasses);
1333
+            for (buttonName in iconOverrideHash) {
1334
+                iconClassesCopy[buttonName] = this.applyIconOverridePrefix(iconOverrideHash[buttonName]);
1335
+            }
1336
+            this.iconClasses = iconClassesCopy;
1337
+        }
1338
+        else if (iconOverrideHash === false) {
1339
+            this.iconClasses = {};
1340
+        }
1341
+    };
1342
+    Theme.prototype.applyIconOverridePrefix = function (className) {
1343
+        var prefix = this.iconOverridePrefix;
1344
+        if (prefix && className.indexOf(prefix) !== 0) { // if not already present
1345
+            className = prefix + className;
1346
+        }
1347
+        return className;
1348
+    };
1349
+    Theme.prototype.getClass = function (key) {
1350
+        return this.classes[key] || '';
1351
+    };
1352
+    Theme.prototype.getIconClass = function (buttonName) {
1353
+        var className = this.iconClasses[buttonName];
1354
+        if (className) {
1355
+            return this.baseIconClass + ' ' + className;
1356
+        }
1357
+        return '';
1358
+    };
1359
+    Theme.prototype.getCustomButtonIconClass = function (customButtonProps) {
1360
+        var className;
1361
+        if (this.iconOverrideCustomButtonOption) {
1362
+            className = customButtonProps[this.iconOverrideCustomButtonOption];
1363
+            if (className) {
1364
+                return this.baseIconClass + ' ' + this.applyIconOverridePrefix(className);
1365
+            }
1366
+        }
1367
+        return '';
1368
+    };
1369
+    return Theme;
1370
+}());
1371
+exports.default = Theme;
1372
+Theme.prototype.classes = {};
1373
+Theme.prototype.iconClasses = {};
1374
+Theme.prototype.baseIconClass = '';
1375
+Theme.prototype.iconOverridePrefix = '';
1376
+
1377
+
1378
+/***/ }),
1379
+/* 23 */
1380
+/***/ (function(module, exports, __webpack_require__) {
1381
+
1382
+Object.defineProperty(exports, "__esModule", { value: true });
1383
+var $ = __webpack_require__(3);
1384
+var exportHooks = __webpack_require__(18);
1385
+var EmitterMixin_1 = __webpack_require__(13);
1386
+var ListenerMixin_1 = __webpack_require__(7);
1387
+exportHooks.touchMouseIgnoreWait = 500;
1388
+var globalEmitter = null;
1389
+var neededCount = 0;
1390
+/*
1391
+Listens to document and window-level user-interaction events, like touch events and mouse events,
1392
+and fires these events as-is to whoever is observing a GlobalEmitter.
1393
+Best when used as a singleton via GlobalEmitter.get()
1394
+
1395
+Normalizes mouse/touch events. For examples:
1396
+- ignores the the simulated mouse events that happen after a quick tap: mousemove+mousedown+mouseup+click
1397
+- compensates for various buggy scenarios where a touchend does not fire
1398
+*/
1399
+var GlobalEmitter = /** @class */ (function () {
1400
+    function GlobalEmitter() {
1401
+        this.isTouching = false;
1402
+        this.mouseIgnoreDepth = 0;
1403
     }
1404
     // gets the singleton
1405
     GlobalEmitter.get = function () {
1406
@@ -2179,7 +2479,7 @@
1407
     // called when the object that originally called needed() doesn't need a GlobalEmitter anymore.
1408
     GlobalEmitter.unneeded = function () {
1409
         neededCount--;
1410
-        if (!neededCount) {
1411
+        if (!neededCount) { // nobody else needs it
1412
             globalEmitter.unbind();
1413
             globalEmitter = null;
1414
         }
1415
@@ -2214,7 +2514,8 @@
1416
     };
1417
     GlobalEmitter.prototype.unbind = function () {
1418
         this.stopListeningTo($(document));
1419
-        window.removeEventListener('touchmove', this.handleTouchMoveProxy);
1420
+        window.removeEventListener('touchmove', this.handleTouchMoveProxy, { passive: false } // use same options as addEventListener
1421
+        );
1422
         window.removeEventListener('scroll', this.handleScrollProxy, true // useCapture
1423
         );
1424
     };
1425
@@ -2309,11 +2610,11 @@
1426
 
1427
 
1428
 /***/ }),
1429
-/* 22 */
1430
+/* 24 */
1431
 /***/ (function(module, exports, __webpack_require__) {
1432
 
1433
 Object.defineProperty(exports, "__esModule", { value: true });
1434
-var exportHooks = __webpack_require__(16);
1435
+var exportHooks = __webpack_require__(18);
1436
 exports.viewHash = {};
1437
 exportHooks.views = exports.viewHash;
1438
 function defineView(viewName, viewConfig) {
1439
@@ -2327,184 +2628,21 @@
1440
 
1441
 
1442
 /***/ }),
1443
-/* 23 */
1444
-/***/ (function(module, exports, __webpack_require__) {
1445
-
1446
-Object.defineProperty(exports, "__esModule", { value: true });
1447
-var tslib_1 = __webpack_require__(2);
1448
-var util_1 = __webpack_require__(4);
1449
-var DragListener_1 = __webpack_require__(54);
1450
-/* Tracks mouse movements over a component and raises events about which hit the mouse is over.
1451
-------------------------------------------------------------------------------------------------------------------------
1452
-options:
1453
-- subjectEl
1454
-- subjectCenter
1455
-*/
1456
-var HitDragListener = /** @class */ (function (_super) {
1457
-    tslib_1.__extends(HitDragListener, _super);
1458
-    function HitDragListener(component, options) {
1459
-        var _this = _super.call(this, options) || this;
1460
-        _this.component = component;
1461
-        return _this;
1462
-    }
1463
-    // Called when drag listening starts (but a real drag has not necessarily began).
1464
-    // ev might be undefined if dragging was started manually.
1465
-    HitDragListener.prototype.handleInteractionStart = function (ev) {
1466
-        var subjectEl = this.subjectEl;
1467
-        var subjectRect;
1468
-        var origPoint;
1469
-        var point;
1470
-        this.component.hitsNeeded();
1471
-        this.computeScrollBounds(); // for autoscroll
1472
-        if (ev) {
1473
-            origPoint = { left: util_1.getEvX(ev), top: util_1.getEvY(ev) };
1474
-            point = origPoint;
1475
-            // constrain the point to bounds of the element being dragged
1476
-            if (subjectEl) {
1477
-                subjectRect = util_1.getOuterRect(subjectEl); // used for centering as well
1478
-                point = util_1.constrainPoint(point, subjectRect);
1479
-            }
1480
-            this.origHit = this.queryHit(point.left, point.top);
1481
-            // treat the center of the subject as the collision point?
1482
-            if (subjectEl && this.options.subjectCenter) {
1483
-                // only consider the area the subject overlaps the hit. best for large subjects.
1484
-                // TODO: skip this if hit didn't supply left/right/top/bottom
1485
-                if (this.origHit) {
1486
-                    subjectRect = util_1.intersectRects(this.origHit, subjectRect) ||
1487
-                        subjectRect; // in case there is no intersection
1488
-                }
1489
-                point = util_1.getRectCenter(subjectRect);
1490
-            }
1491
-            this.coordAdjust = util_1.diffPoints(point, origPoint); // point - origPoint
1492
-        }
1493
-        else {
1494
-            this.origHit = null;
1495
-            this.coordAdjust = null;
1496
-        }
1497
-        // call the super-method. do it after origHit has been computed
1498
-        _super.prototype.handleInteractionStart.call(this, ev);
1499
-    };
1500
-    // Called when the actual drag has started
1501
-    HitDragListener.prototype.handleDragStart = function (ev) {
1502
-        var hit;
1503
-        _super.prototype.handleDragStart.call(this, ev);
1504
-        // might be different from this.origHit if the min-distance is large
1505
-        hit = this.queryHit(util_1.getEvX(ev), util_1.getEvY(ev));
1506
-        // report the initial hit the mouse is over
1507
-        // especially important if no min-distance and drag starts immediately
1508
-        if (hit) {
1509
-            this.handleHitOver(hit);
1510
-        }
1511
-    };
1512
-    // Called when the drag moves
1513
-    HitDragListener.prototype.handleDrag = function (dx, dy, ev) {
1514
-        var hit;
1515
-        _super.prototype.handleDrag.call(this, dx, dy, ev);
1516
-        hit = this.queryHit(util_1.getEvX(ev), util_1.getEvY(ev));
1517
-        if (!isHitsEqual(hit, this.hit)) {
1518
-            if (this.hit) {
1519
-                this.handleHitOut();
1520
-            }
1521
-            if (hit) {
1522
-                this.handleHitOver(hit);
1523
-            }
1524
-        }
1525
-    };
1526
-    // Called when dragging has been stopped
1527
-    HitDragListener.prototype.handleDragEnd = function (ev) {
1528
-        this.handleHitDone();
1529
-        _super.prototype.handleDragEnd.call(this, ev);
1530
-    };
1531
-    // Called when a the mouse has just moved over a new hit
1532
-    HitDragListener.prototype.handleHitOver = function (hit) {
1533
-        var isOrig = isHitsEqual(hit, this.origHit);
1534
-        this.hit = hit;
1535
-        this.trigger('hitOver', this.hit, isOrig, this.origHit);
1536
-    };
1537
-    // Called when the mouse has just moved out of a hit
1538
-    HitDragListener.prototype.handleHitOut = function () {
1539
-        if (this.hit) {
1540
-            this.trigger('hitOut', this.hit);
1541
-            this.handleHitDone();
1542
-            this.hit = null;
1543
-        }
1544
-    };
1545
-    // Called after a hitOut. Also called before a dragStop
1546
-    HitDragListener.prototype.handleHitDone = function () {
1547
-        if (this.hit) {
1548
-            this.trigger('hitDone', this.hit);
1549
-        }
1550
-    };
1551
-    // Called when the interaction ends, whether there was a real drag or not
1552
-    HitDragListener.prototype.handleInteractionEnd = function (ev, isCancelled) {
1553
-        _super.prototype.handleInteractionEnd.call(this, ev, isCancelled);
1554
-        this.origHit = null;
1555
-        this.hit = null;
1556
-        this.component.hitsNotNeeded();
1557
-    };
1558
-    // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
1559
-    HitDragListener.prototype.handleScrollEnd = function () {
1560
-        _super.prototype.handleScrollEnd.call(this);
1561
-        // hits' absolute positions will be in new places after a user's scroll.
1562
-        // HACK for recomputing.
1563
-        if (this.isDragging) {
1564
-            this.component.releaseHits();
1565
-            this.component.prepareHits();
1566
-        }
1567
-    };
1568
-    // Gets the hit underneath the coordinates for the given mouse event
1569
-    HitDragListener.prototype.queryHit = function (left, top) {
1570
-        if (this.coordAdjust) {
1571
-            left += this.coordAdjust.left;
1572
-            top += this.coordAdjust.top;
1573
-        }
1574
-        return this.component.queryHit(left, top);
1575
-    };
1576
-    return HitDragListener;
1577
-}(DragListener_1.default));
1578
-exports.default = HitDragListener;
1579
-// Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component.
1580
-// Two null values will be considered equal, as two "out of the component" states are the same.
1581
-function isHitsEqual(hit0, hit1) {
1582
-    if (!hit0 && !hit1) {
1583
-        return true;
1584
-    }
1585
-    if (hit0 && hit1) {
1586
-        return hit0.component === hit1.component &&
1587
-            isHitPropsWithin(hit0, hit1) &&
1588
-            isHitPropsWithin(hit1, hit0); // ensures all props are identical
1589
-    }
1590
-    return false;
1591
-}
1592
-// Returns true if all of subHit's non-standard properties are within superHit
1593
-function isHitPropsWithin(subHit, superHit) {
1594
-    for (var propName in subHit) {
1595
-        if (!/^(component|left|right|top|bottom)$/.test(propName)) {
1596
-            if (subHit[propName] !== superHit[propName]) {
1597
-                return false;
1598
-            }
1599
-        }
1600
-    }
1601
-    return true;
1602
-}
1603
-
1604
-
1605
-/***/ }),
1606
-/* 24 */,
1607
 /* 25 */,
1608
 /* 26 */,
1609
 /* 27 */,
1610
 /* 28 */,
1611
 /* 29 */,
1612
 /* 30 */,
1613
-/* 31 */
1614
+/* 31 */,
1615
+/* 32 */
1616
 /***/ (function(module, exports, __webpack_require__) {
1617
 
1618
 Object.defineProperty(exports, "__esModule", { value: true });
1619
 var $ = __webpack_require__(3);
1620
 var moment = __webpack_require__(0);
1621
-var exportHooks = __webpack_require__(16);
1622
-var options_1 = __webpack_require__(32);
1623
+var exportHooks = __webpack_require__(18);
1624
+var options_1 = __webpack_require__(33);
1625
 var util_1 = __webpack_require__(4);
1626
 exports.localeOptionHash = {};
1627
 exportHooks.locales = exports.localeOptionHash;
1628
@@ -2667,7 +2805,7 @@
1629
 
1630
 
1631
 /***/ }),
1632
-/* 32 */
1633
+/* 33 */
1634
 /***/ (function(module, exports, __webpack_require__) {
1635
 
1636
 Object.defineProperty(exports, "__esModule", { value: true });
1637
@@ -2781,14 +2919,35 @@
1638
 
1639
 
1640
 /***/ }),
1641
-/* 33 */
1642
-/***/ (function(module, exports, __webpack_require__) {
1643
+/* 34 */
1644
+/***/ (function(module, exports) {
1645
 
1646
 Object.defineProperty(exports, "__esModule", { value: true });
1647
-var tslib_1 = __webpack_require__(2);
1648
-var util_1 = __webpack_require__(4);
1649
-// Class that all other classes will inherit from
1650
-var Class = /** @class */ (function () {
1651
+var EventFootprint = /** @class */ (function () {
1652
+    function EventFootprint(componentFootprint, eventDef, eventInstance) {
1653
+        this.componentFootprint = componentFootprint;
1654
+        this.eventDef = eventDef;
1655
+        if (eventInstance) {
1656
+            this.eventInstance = eventInstance;
1657
+        }
1658
+    }
1659
+    EventFootprint.prototype.getEventLegacy = function () {
1660
+        return (this.eventInstance || this.eventDef).toLegacy();
1661
+    };
1662
+    return EventFootprint;
1663
+}());
1664
+exports.default = EventFootprint;
1665
+
1666
+
1667
+/***/ }),
1668
+/* 35 */
1669
+/***/ (function(module, exports, __webpack_require__) {
1670
+
1671
+Object.defineProperty(exports, "__esModule", { value: true });
1672
+var tslib_1 = __webpack_require__(2);
1673
+var util_1 = __webpack_require__(4);
1674
+// Class that all other classes will inherit from
1675
+var Class = /** @class */ (function () {
1676
     function Class() {
1677
     }
1678
     // Called on a class to create a subclass.
1679
@@ -2815,12 +2974,34 @@
1680
 
1681
 
1682
 /***/ }),
1683
-/* 34 */
1684
+/* 36 */
1685
+/***/ (function(module, exports, __webpack_require__) {
1686
+
1687
+Object.defineProperty(exports, "__esModule", { value: true });
1688
+var moment = __webpack_require__(0);
1689
+var util_1 = __webpack_require__(4);
1690
+var SingleEventDef_1 = __webpack_require__(9);
1691
+var RecurringEventDef_1 = __webpack_require__(54);
1692
+exports.default = {
1693
+    parse: function (eventInput, source) {
1694
+        if (util_1.isTimeString(eventInput.start) || moment.isDuration(eventInput.start) ||
1695
+            util_1.isTimeString(eventInput.end) || moment.isDuration(eventInput.end)) {
1696
+            return RecurringEventDef_1.default.parse(eventInput, source);
1697
+        }
1698
+        else {
1699
+            return SingleEventDef_1.default.parse(eventInput, source);
1700
+        }
1701
+    }
1702
+};
1703
+
1704
+
1705
+/***/ }),
1706
+/* 37 */
1707
 /***/ (function(module, exports, __webpack_require__) {
1708
 
1709
 Object.defineProperty(exports, "__esModule", { value: true });
1710
 var $ = __webpack_require__(3);
1711
-var ParsableModelMixin_1 = __webpack_require__(208);
1712
+var ParsableModelMixin_1 = __webpack_require__(52);
1713
 var EventDef = /** @class */ (function () {
1714
     function EventDef(source) {
1715
         this.source = source;
1716
@@ -2918,7 +3099,7 @@
1717
         else {
1718
             this.id = EventDef.generateId();
1719
         }
1720
-        if (rawProps._id != null) {
1721
+        if (rawProps._id != null) { // accept this prop, even tho somewhat internal
1722
             this.uid = String(rawProps._id);
1723
         }
1724
         else {
1725
@@ -2966,73 +3147,39 @@
1726
 
1727
 
1728
 /***/ }),
1729
-/* 35 */
1730
-/***/ (function(module, exports, __webpack_require__) {
1731
-
1732
-Object.defineProperty(exports, "__esModule", { value: true });
1733
-var EventRange_1 = __webpack_require__(211);
1734
-var EventFootprint_1 = __webpack_require__(36);
1735
-var ComponentFootprint_1 = __webpack_require__(12);
1736
-function eventDefsToEventInstances(eventDefs, unzonedRange) {
1737
-    var eventInstances = [];
1738
-    var i;
1739
-    for (i = 0; i < eventDefs.length; i++) {
1740
-        eventInstances.push.apply(eventInstances, // append
1741
-        eventDefs[i].buildInstances(unzonedRange));
1742
-    }
1743
-    return eventInstances;
1744
-}
1745
-exports.eventDefsToEventInstances = eventDefsToEventInstances;
1746
-function eventInstanceToEventRange(eventInstance) {
1747
-    return new EventRange_1.default(eventInstance.dateProfile.unzonedRange, eventInstance.def, eventInstance);
1748
-}
1749
-exports.eventInstanceToEventRange = eventInstanceToEventRange;
1750
-function eventRangeToEventFootprint(eventRange) {
1751
-    return new EventFootprint_1.default(new ComponentFootprint_1.default(eventRange.unzonedRange, eventRange.eventDef.isAllDay()), eventRange.eventDef, eventRange.eventInstance // might not exist
1752
-    );
1753
-}
1754
-exports.eventRangeToEventFootprint = eventRangeToEventFootprint;
1755
-function eventInstanceToUnzonedRange(eventInstance) {
1756
-    return eventInstance.dateProfile.unzonedRange;
1757
-}
1758
-exports.eventInstanceToUnzonedRange = eventInstanceToUnzonedRange;
1759
-function eventFootprintToComponentFootprint(eventFootprint) {
1760
-    return eventFootprint.componentFootprint;
1761
-}
1762
-exports.eventFootprintToComponentFootprint = eventFootprintToComponentFootprint;
1763
-
1764
-
1765
-/***/ }),
1766
-/* 36 */
1767
+/* 38 */
1768
 /***/ (function(module, exports) {
1769
 
1770
 Object.defineProperty(exports, "__esModule", { value: true });
1771
-var EventFootprint = /** @class */ (function () {
1772
-    function EventFootprint(componentFootprint, eventDef, eventInstance) {
1773
-        this.componentFootprint = componentFootprint;
1774
-        this.eventDef = eventDef;
1775
-        if (eventInstance) {
1776
-            this.eventInstance = eventInstance;
1777
+exports.default = {
1778
+    sourceClasses: [],
1779
+    registerClass: function (EventSourceClass) {
1780
+        this.sourceClasses.unshift(EventSourceClass); // give highest priority
1781
+    },
1782
+    parse: function (rawInput, calendar) {
1783
+        var sourceClasses = this.sourceClasses;
1784
+        var i;
1785
+        var eventSource;
1786
+        for (i = 0; i < sourceClasses.length; i++) {
1787
+            eventSource = sourceClasses[i].parse(rawInput, calendar);
1788
+            if (eventSource) {
1789
+                return eventSource;
1790
+            }
1791
         }
1792
     }
1793
-    EventFootprint.prototype.getEventLegacy = function () {
1794
-        return (this.eventInstance || this.eventDef).toLegacy();
1795
-    };
1796
-    return EventFootprint;
1797
-}());
1798
-exports.default = EventFootprint;
1799
+};
1800
 
1801
 
1802
 /***/ }),
1803
-/* 37 */
1804
+/* 39 */
1805
 /***/ (function(module, exports, __webpack_require__) {
1806
 
1807
 Object.defineProperty(exports, "__esModule", { value: true });
1808
 var util_1 = __webpack_require__(4);
1809
-var EventDateProfile_1 = __webpack_require__(17);
1810
-var EventDef_1 = __webpack_require__(34);
1811
-var EventDefDateMutation_1 = __webpack_require__(50);
1812
-var SingleEventDef_1 = __webpack_require__(13);
1813
+var EventDateProfile_1 = __webpack_require__(16);
1814
+var EventDef_1 = __webpack_require__(37);
1815
+var EventDefDateMutation_1 = __webpack_require__(40);
1816
+var SingleEventDef_1 = __webpack_require__(9);
1817
 var EventDefMutation = /** @class */ (function () {
1818
     function EventDefMutation() {
1819
     }
1820
@@ -3055,12 +3202,12 @@
1821
             else if (eventDef.isStandardProp(propName)) {
1822
                 standardProps[propName] = rawProps[propName];
1823
             }
1824
-            else if (eventDef.miscProps[propName] !== rawProps[propName]) {
1825
+            else if (eventDef.miscProps[propName] !== rawProps[propName]) { // only if changed
1826
                 miscProps[propName] = rawProps[propName];
1827
             }
1828
         }
1829
         dateProfile = EventDateProfile_1.default.parse(dateProps, eventDef.source);
1830
-        if (dateProfile) {
1831
+        if (dateProfile) { // no failure?
1832
             dateMutation = EventDefDateMutation_1.default.createFromDiff(eventInstance.dateProfile, dateProfile, largeUnit);
1833
         }
1834
         if (standardProps.id !== eventDef.id) {
1835
@@ -3138,127 +3285,245 @@
1836
 
1837
 
1838
 /***/ }),
1839
-/* 38 */
1840
-/***/ (function(module, exports) {
1841
-
1842
-Object.defineProperty(exports, "__esModule", { value: true });
1843
-exports.default = {
1844
-    sourceClasses: [],
1845
-    registerClass: function (EventSourceClass) {
1846
-        this.sourceClasses.unshift(EventSourceClass); // give highest priority
1847
-    },
1848
-    parse: function (rawInput, calendar) {
1849
-        var sourceClasses = this.sourceClasses;
1850
-        var i;
1851
-        var eventSource;
1852
-        for (i = 0; i < sourceClasses.length; i++) {
1853
-            eventSource = sourceClasses[i].parse(rawInput, calendar);
1854
-            if (eventSource) {
1855
-                return eventSource;
1856
-            }
1857
-        }
1858
-    }
1859
-};
1860
-
1861
-
1862
-/***/ }),
1863
-/* 39 */
1864
+/* 40 */
1865
 /***/ (function(module, exports, __webpack_require__) {
1866
 
1867
 Object.defineProperty(exports, "__esModule", { value: true });
1868
-var tslib_1 = __webpack_require__(2);
1869
-var $ = __webpack_require__(3);
1870
 var util_1 = __webpack_require__(4);
1871
-var Class_1 = __webpack_require__(33);
1872
-/*
1873
-Embodies a div that has potential scrollbars
1874
-*/
1875
-var Scroller = /** @class */ (function (_super) {
1876
-    tslib_1.__extends(Scroller, _super);
1877
-    function Scroller(options) {
1878
-        var _this = _super.call(this) || this;
1879
-        options = options || {};
1880
-        _this.overflowX = options.overflowX || options.overflow || 'auto';
1881
-        _this.overflowY = options.overflowY || options.overflow || 'auto';
1882
-        return _this;
1883
+var EventDateProfile_1 = __webpack_require__(16);
1884
+var EventDefDateMutation = /** @class */ (function () {
1885
+    function EventDefDateMutation() {
1886
+        this.clearEnd = false;
1887
+        this.forceTimed = false;
1888
+        this.forceAllDay = false;
1889
     }
1890
-    Scroller.prototype.render = function () {
1891
-        this.el = this.renderEl();
1892
-        this.applyOverflow();
1893
-    };
1894
-    Scroller.prototype.renderEl = function () {
1895
-        return (this.scrollEl = $('<div class="fc-scroller"></div>'));
1896
-    };
1897
-    // sets to natural height, unlocks overflow
1898
-    Scroller.prototype.clear = function () {
1899
-        this.setHeight('auto');
1900
-        this.applyOverflow();
1901
-    };
1902
-    Scroller.prototype.destroy = function () {
1903
-        this.el.remove();
1904
-    };
1905
-    // Overflow
1906
-    // -----------------------------------------------------------------------------------------------------------------
1907
-    Scroller.prototype.applyOverflow = function () {
1908
-        this.scrollEl.css({
1909
-            'overflow-x': this.overflowX,
1910
-            'overflow-y': this.overflowY
1911
-        });
1912
-    };
1913
-    // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
1914
-    // Useful for preserving scrollbar widths regardless of future resizes.
1915
-    // Can pass in scrollbarWidths for optimization.
1916
-    Scroller.prototype.lockOverflow = function (scrollbarWidths) {
1917
-        var overflowX = this.overflowX;
1918
-        var overflowY = this.overflowY;
1919
-        scrollbarWidths = scrollbarWidths || this.getScrollbarWidths();
1920
-        if (overflowX === 'auto') {
1921
-            overflowX = (scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars?
1922
-                // OR scrolling pane with massless scrollbars?
1923
-                this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth) ? 'scroll' : 'hidden';
1924
+    EventDefDateMutation.createFromDiff = function (dateProfile0, dateProfile1, largeUnit) {
1925
+        var clearEnd = dateProfile0.end && !dateProfile1.end;
1926
+        var forceTimed = dateProfile0.isAllDay() && !dateProfile1.isAllDay();
1927
+        var forceAllDay = !dateProfile0.isAllDay() && dateProfile1.isAllDay();
1928
+        var dateDelta;
1929
+        var endDiff;
1930
+        var endDelta;
1931
+        var mutation;
1932
+        // subtracts the dates in the appropriate way, returning a duration
1933
+        function subtractDates(date1, date0) {
1934
+            if (largeUnit) {
1935
+                return util_1.diffByUnit(date1, date0, largeUnit); // poorly named
1936
+            }
1937
+            else if (dateProfile1.isAllDay()) {
1938
+                return util_1.diffDay(date1, date0); // poorly named
1939
+            }
1940
+            else {
1941
+                return util_1.diffDayTime(date1, date0); // poorly named
1942
+            }
1943
         }
1944
-        if (overflowY === 'auto') {
1945
-            overflowY = (scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars?
1946
-                // OR scrolling pane with massless scrollbars?
1947
-                this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight) ? 'scroll' : 'hidden';
1948
+        dateDelta = subtractDates(dateProfile1.start, dateProfile0.start);
1949
+        if (dateProfile1.end) {
1950
+            // use unzonedRanges because dateProfile0.end might be null
1951
+            endDiff = subtractDates(dateProfile1.unzonedRange.getEnd(), dateProfile0.unzonedRange.getEnd());
1952
+            endDelta = endDiff.subtract(dateDelta);
1953
         }
1954
-        this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY });
1955
-    };
1956
-    // Getters / Setters
1957
-    // -----------------------------------------------------------------------------------------------------------------
1958
-    Scroller.prototype.setHeight = function (height) {
1959
-        this.scrollEl.height(height);
1960
-    };
1961
-    Scroller.prototype.getScrollTop = function () {
1962
-        return this.scrollEl.scrollTop();
1963
-    };
1964
-    Scroller.prototype.setScrollTop = function (top) {
1965
-        this.scrollEl.scrollTop(top);
1966
-    };
1967
-    Scroller.prototype.getClientWidth = function () {
1968
-        return this.scrollEl[0].clientWidth;
1969
-    };
1970
-    Scroller.prototype.getClientHeight = function () {
1971
-        return this.scrollEl[0].clientHeight;
1972
-    };
1973
-    Scroller.prototype.getScrollbarWidths = function () {
1974
-        return util_1.getScrollbarWidths(this.scrollEl);
1975
+        mutation = new EventDefDateMutation();
1976
+        mutation.clearEnd = clearEnd;
1977
+        mutation.forceTimed = forceTimed;
1978
+        mutation.forceAllDay = forceAllDay;
1979
+        mutation.setDateDelta(dateDelta);
1980
+        mutation.setEndDelta(endDelta);
1981
+        return mutation;
1982
     };
1983
-    return Scroller;
1984
-}(Class_1.default));
1985
-exports.default = Scroller;
1986
-
1987
-
1988
-/***/ }),
1989
-/* 40 */
1990
-/***/ (function(module, exports, __webpack_require__) {
1991
-
1992
-Object.defineProperty(exports, "__esModule", { value: true });
1993
+    /*
1994
+    returns an undo function.
1995
+    */
1996
+    EventDefDateMutation.prototype.buildNewDateProfile = function (eventDateProfile, calendar) {
1997
+        var start = eventDateProfile.start.clone();
1998
+        var end = null;
1999
+        var shouldRezone = false;
2000
+        if (eventDateProfile.end && !this.clearEnd) {
2001
+            end = eventDateProfile.end.clone();
2002
+        }
2003
+        else if (this.endDelta && !end) {
2004
+            end = calendar.getDefaultEventEnd(eventDateProfile.isAllDay(), start);
2005
+        }
2006
+        if (this.forceTimed) {
2007
+            shouldRezone = true;
2008
+            if (!start.hasTime()) {
2009
+                start.time(0);
2010
+            }
2011
+            if (end && !end.hasTime()) {
2012
+                end.time(0);
2013
+            }
2014
+        }
2015
+        else if (this.forceAllDay) {
2016
+            if (start.hasTime()) {
2017
+                start.stripTime();
2018
+            }
2019
+            if (end && end.hasTime()) {
2020
+                end.stripTime();
2021
+            }
2022
+        }
2023
+        if (this.dateDelta) {
2024
+            shouldRezone = true;
2025
+            start.add(this.dateDelta);
2026
+            if (end) {
2027
+                end.add(this.dateDelta);
2028
+            }
2029
+        }
2030
+        // do this before adding startDelta to start, so we can work off of start
2031
+        if (this.endDelta) {
2032
+            shouldRezone = true;
2033
+            end.add(this.endDelta);
2034
+        }
2035
+        if (this.startDelta) {
2036
+            shouldRezone = true;
2037
+            start.add(this.startDelta);
2038
+        }
2039
+        if (shouldRezone) {
2040
+            start = calendar.applyTimezone(start);
2041
+            if (end) {
2042
+                end = calendar.applyTimezone(end);
2043
+            }
2044
+        }
2045
+        // TODO: okay to access calendar option?
2046
+        if (!end && calendar.opt('forceEventDuration')) {
2047
+            end = calendar.getDefaultEventEnd(eventDateProfile.isAllDay(), start);
2048
+        }
2049
+        return new EventDateProfile_1.default(start, end, calendar);
2050
+    };
2051
+    EventDefDateMutation.prototype.setDateDelta = function (dateDelta) {
2052
+        if (dateDelta && dateDelta.valueOf()) {
2053
+            this.dateDelta = dateDelta;
2054
+        }
2055
+        else {
2056
+            this.dateDelta = null;
2057
+        }
2058
+    };
2059
+    EventDefDateMutation.prototype.setStartDelta = function (startDelta) {
2060
+        if (startDelta && startDelta.valueOf()) {
2061
+            this.startDelta = startDelta;
2062
+        }
2063
+        else {
2064
+            this.startDelta = null;
2065
+        }
2066
+    };
2067
+    EventDefDateMutation.prototype.setEndDelta = function (endDelta) {
2068
+        if (endDelta && endDelta.valueOf()) {
2069
+            this.endDelta = endDelta;
2070
+        }
2071
+        else {
2072
+            this.endDelta = null;
2073
+        }
2074
+    };
2075
+    EventDefDateMutation.prototype.isEmpty = function () {
2076
+        return !this.clearEnd && !this.forceTimed && !this.forceAllDay &&
2077
+            !this.dateDelta && !this.startDelta && !this.endDelta;
2078
+    };
2079
+    return EventDefDateMutation;
2080
+}());
2081
+exports.default = EventDefDateMutation;
2082
+
2083
+
2084
+/***/ }),
2085
+/* 41 */
2086
+/***/ (function(module, exports, __webpack_require__) {
2087
+
2088
+Object.defineProperty(exports, "__esModule", { value: true });
2089
+var tslib_1 = __webpack_require__(2);
2090
+var $ = __webpack_require__(3);
2091
+var util_1 = __webpack_require__(4);
2092
+var Class_1 = __webpack_require__(35);
2093
+/*
2094
+Embodies a div that has potential scrollbars
2095
+*/
2096
+var Scroller = /** @class */ (function (_super) {
2097
+    tslib_1.__extends(Scroller, _super);
2098
+    function Scroller(options) {
2099
+        var _this = _super.call(this) || this;
2100
+        options = options || {};
2101
+        _this.overflowX = options.overflowX || options.overflow || 'auto';
2102
+        _this.overflowY = options.overflowY || options.overflow || 'auto';
2103
+        return _this;
2104
+    }
2105
+    Scroller.prototype.render = function () {
2106
+        this.el = this.renderEl();
2107
+        this.applyOverflow();
2108
+    };
2109
+    Scroller.prototype.renderEl = function () {
2110
+        return (this.scrollEl = $('<div class="fc-scroller"></div>'));
2111
+    };
2112
+    // sets to natural height, unlocks overflow
2113
+    Scroller.prototype.clear = function () {
2114
+        this.setHeight('auto');
2115
+        this.applyOverflow();
2116
+    };
2117
+    Scroller.prototype.destroy = function () {
2118
+        this.el.remove();
2119
+    };
2120
+    // Overflow
2121
+    // -----------------------------------------------------------------------------------------------------------------
2122
+    Scroller.prototype.applyOverflow = function () {
2123
+        this.scrollEl.css({
2124
+            'overflow-x': this.overflowX,
2125
+            'overflow-y': this.overflowY
2126
+        });
2127
+    };
2128
+    // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
2129
+    // Useful for preserving scrollbar widths regardless of future resizes.
2130
+    // Can pass in scrollbarWidths for optimization.
2131
+    Scroller.prototype.lockOverflow = function (scrollbarWidths) {
2132
+        var overflowX = this.overflowX;
2133
+        var overflowY = this.overflowY;
2134
+        scrollbarWidths = scrollbarWidths || this.getScrollbarWidths();
2135
+        if (overflowX === 'auto') {
2136
+            overflowX = (scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars?
2137
+                // OR scrolling pane with massless scrollbars?
2138
+                this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth
2139
+            // subtract 1 because of IE off-by-one issue
2140
+            ) ? 'scroll' : 'hidden';
2141
+        }
2142
+        if (overflowY === 'auto') {
2143
+            overflowY = (scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars?
2144
+                // OR scrolling pane with massless scrollbars?
2145
+                this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight
2146
+            // subtract 1 because of IE off-by-one issue
2147
+            ) ? 'scroll' : 'hidden';
2148
+        }
2149
+        this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY });
2150
+    };
2151
+    // Getters / Setters
2152
+    // -----------------------------------------------------------------------------------------------------------------
2153
+    Scroller.prototype.setHeight = function (height) {
2154
+        this.scrollEl.height(height);
2155
+    };
2156
+    Scroller.prototype.getScrollTop = function () {
2157
+        return this.scrollEl.scrollTop();
2158
+    };
2159
+    Scroller.prototype.setScrollTop = function (top) {
2160
+        this.scrollEl.scrollTop(top);
2161
+    };
2162
+    Scroller.prototype.getClientWidth = function () {
2163
+        return this.scrollEl[0].clientWidth;
2164
+    };
2165
+    Scroller.prototype.getClientHeight = function () {
2166
+        return this.scrollEl[0].clientHeight;
2167
+    };
2168
+    Scroller.prototype.getScrollbarWidths = function () {
2169
+        return util_1.getScrollbarWidths(this.scrollEl);
2170
+    };
2171
+    return Scroller;
2172
+}(Class_1.default));
2173
+exports.default = Scroller;
2174
+
2175
+
2176
+/***/ }),
2177
+/* 42 */
2178
+/***/ (function(module, exports, __webpack_require__) {
2179
+
2180
+Object.defineProperty(exports, "__esModule", { value: true });
2181
 var tslib_1 = __webpack_require__(2);
2182
 var $ = __webpack_require__(3);
2183
 var util_1 = __webpack_require__(4);
2184
-var DateComponent_1 = __webpack_require__(219);
2185
-var GlobalEmitter_1 = __webpack_require__(21);
2186
+var DateComponent_1 = __webpack_require__(231);
2187
+var GlobalEmitter_1 = __webpack_require__(23);
2188
 var InteractiveDateComponent = /** @class */ (function (_super) {
2189
     tslib_1.__extends(InteractiveDateComponent, _super);
2190
     function InteractiveDateComponent(_view, _options) {
2191
@@ -3505,7 +3770,7 @@
2192
 
2193
 
2194
 /***/ }),
2195
-/* 41 */
2196
+/* 43 */
2197
 /***/ (function(module, exports, __webpack_require__) {
2198
 
2199
 Object.defineProperty(exports, "__esModule", { value: true });
2200
@@ -3513,10 +3778,10 @@
2201
 var $ = __webpack_require__(3);
2202
 var moment = __webpack_require__(0);
2203
 var util_1 = __webpack_require__(4);
2204
-var RenderQueue_1 = __webpack_require__(218);
2205
-var DateProfileGenerator_1 = __webpack_require__(221);
2206
-var InteractiveDateComponent_1 = __webpack_require__(40);
2207
-var GlobalEmitter_1 = __webpack_require__(21);
2208
+var RenderQueue_1 = __webpack_require__(229);
2209
+var DateProfileGenerator_1 = __webpack_require__(55);
2210
+var InteractiveDateComponent_1 = __webpack_require__(42);
2211
+var GlobalEmitter_1 = __webpack_require__(23);
2212
 var UnzonedRange_1 = __webpack_require__(5);
2213
 /* An abstract class from which other views inherit from
2214
 ----------------------------------------------------------------------------------------------------------------------*/
2215
@@ -3566,7 +3831,7 @@
2216
         this.addScroll(this.queryScroll());
2217
     };
2218
     View.prototype.onRenderQueueStop = function () {
2219
-        if (this.calendar.updateViewSize()) {
2220
+        if (this.calendar.updateViewSize()) { // success?
2221
             this.popScroll();
2222
         }
2223
         this.calendar.thawContentHeight();
2224
@@ -3602,7 +3867,7 @@
2225
         if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
2226
             unzonedRange = dateProfile.currentUnzonedRange;
2227
         }
2228
-        else {
2229
+        else { // for day units or smaller, use the actual day range
2230
             unzonedRange = dateProfile.activeUnzonedRange;
2231
         }
2232
         return this.formatRange({
2233
@@ -3828,7 +4093,7 @@
2234
     /* Dimensions
2235
     ------------------------------------------------------------------------------------------------------------------*/
2236
     View.prototype.updateSize = function (totalHeight, isAuto, isResize) {
2237
-        if (this['setHeight']) {
2238
+        if (this['setHeight']) { // for legacy API
2239
             this['setHeight'](totalHeight, isAuto);
2240
         }
2241
         else {
2242
@@ -3945,15 +4210,16 @@
2243
         var undoFunc = eventManager.mutateEventsWithId(eventInstance.def.id, eventMutation);
2244
         // update the EventInstance, for handlers
2245
         eventInstance.dateProfile = eventMutation.dateMutation.buildNewDateProfile(eventInstance.dateProfile, this.calendar);
2246
-        this.triggerEventResize(eventInstance, eventMutation.dateMutation.endDelta, undoFunc, el, ev);
2247
+        var resizeDelta = eventMutation.dateMutation.endDelta || eventMutation.dateMutation.startDelta;
2248
+        this.triggerEventResize(eventInstance, resizeDelta, undoFunc, el, ev);
2249
     };
2250
     // Triggers event-resize handlers that have subscribed via the API
2251
-    View.prototype.triggerEventResize = function (eventInstance, durationDelta, undoFunc, el, ev) {
2252
+    View.prototype.triggerEventResize = function (eventInstance, resizeDelta, undoFunc, el, ev) {
2253
         this.publiclyTrigger('eventResize', {
2254
             context: el[0],
2255
             args: [
2256
                 eventInstance.toLegacy(),
2257
-                durationDelta,
2258
+                resizeDelta,
2259
                 undoFunc,
2260
                 ev,
2261
                 {},
2262
@@ -3971,7 +4237,7 @@
2263
         this.reportSelection(footprint, ev);
2264
     };
2265
     View.prototype.renderSelectionFootprint = function (footprint) {
2266
-        if (this['renderSelection']) {
2267
+        if (this['renderSelection']) { // legacy method in custom view classes
2268
             this['renderSelection'](footprint.toLegacy(this.calendar));
2269
         }
2270
         else {
2271
@@ -4030,7 +4296,7 @@
2272
     View.prototype.unselectEventInstance = function () {
2273
         if (this.selectedEventInstance) {
2274
             this.getEventSegs().forEach(function (seg) {
2275
-                if (seg.el) {
2276
+                if (seg.el) { // necessary?
2277
                     seg.el.removeClass('fc-selected');
2278
                 }
2279
             });
2280
@@ -4218,7 +4484,7 @@
2281
 
2282
 
2283
 /***/ }),
2284
-/* 42 */
2285
+/* 44 */
2286
 /***/ (function(module, exports, __webpack_require__) {
2287
 
2288
 Object.defineProperty(exports, "__esModule", { value: true });
2289
@@ -4282,7 +4548,7 @@
2290
         // render an `.el` on each seg
2291
         // returns a subset of the segs. segs that were actually rendered
2292
         segs = this.renderFgSegEls(segs);
2293
-        if (this.renderFgSegs(segs) !== false) {
2294
+        if (this.renderFgSegs(segs) !== false) { // no failure?
2295
             this.fgSegs = segs;
2296
         }
2297
     };
2298
@@ -4293,7 +4559,7 @@
2299
     EventRenderer.prototype.renderBgRanges = function (eventRanges) {
2300
         var eventFootprints = this.component.eventRangesToEventFootprints(eventRanges);
2301
         var segs = this.component.eventFootprintsToSegs(eventFootprints);
2302
-        if (this.renderBgSegs(segs) !== false) {
2303
+        if (this.renderBgSegs(segs) !== false) { // no failure?
2304
             this.bgSegs = segs;
2305
         }
2306
     };
2307
@@ -4349,7 +4615,7 @@
2308
         var html = '';
2309
         var renderedSegs = [];
2310
         var i;
2311
-        if (segs.length) {
2312
+        if (segs.length) { // don't build an empty html string
2313
             // build a large concatenation of event segment HTML
2314
             for (i = 0; i < segs.length; i++) {
2315
                 this.beforeFgSegHtml(segs[i]);
2316
@@ -4360,7 +4626,7 @@
2317
             $(html).each(function (i, node) {
2318
                 var seg = segs[i];
2319
                 var el = $(node);
2320
-                if (hasEventRenderHandlers) {
2321
+                if (hasEventRenderHandlers) { // optimization
2322
                     el = _this.filterEventRenderEl(seg.footprint, el);
2323
                 }
2324
                 if (el) {
2325
@@ -4405,7 +4671,7 @@
2326
             context: legacy,
2327
             args: [legacy, el, this.view]
2328
         });
2329
-        if (custom === false) {
2330
+        if (custom === false) { // means don't render at all
2331
             el = null;
2332
         }
2333
         else if (custom && custom !== true) {
2334
@@ -4543,19 +4809,19 @@
2335
 
2336
 
2337
 /***/ }),
2338
-/* 43 */,
2339
-/* 44 */,
2340
 /* 45 */,
2341
 /* 46 */,
2342
-/* 47 */
2343
+/* 47 */,
2344
+/* 48 */,
2345
+/* 49 */
2346
 /***/ (function(module, exports, __webpack_require__) {
2347
 
2348
 Object.defineProperty(exports, "__esModule", { value: true });
2349
-var moment_ext_1 = __webpack_require__(10);
2350
+var moment_ext_1 = __webpack_require__(11);
2351
 // Plugin
2352
 // -------------------------------------------------------------------------------------------------
2353
 moment_ext_1.newMomentProto.format = function () {
2354
-    if (this._fullCalendar && arguments[0]) {
2355
+    if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
2356
         return formatDate(this, arguments[0]); // our extended formatting
2357
     }
2358
     if (this._ambigTime) {
2359
@@ -4564,7 +4830,7 @@
2360
     if (this._ambigZone) {
2361
         return moment_ext_1.oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss');
2362
     }
2363
-    if (this._fullCalendar) {
2364
+    if (this._fullCalendar) { // enhanced non-ambig moment?
2365
         // moment.format() doesn't ensure english, but we want to.
2366
         return moment_ext_1.oldMomentFormat(englishMoment(this));
2367
     }
2368
@@ -4577,7 +4843,7 @@
2369
     if (this._ambigZone) {
2370
         return moment_ext_1.oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss');
2371
     }
2372
-    if (this._fullCalendar) {
2373
+    if (this._fullCalendar) { // enhanced non-ambig moment?
2374
         // depending on browser, moment might not output english. ensure english.
2375
         // https://github.com/moment/moment/blob/2.18.1/src/lib/moment/format.js#L22
2376
         return moment_ext_1.oldMomentProto.toISOString.apply(englishMoment(this), arguments);
2377
@@ -4747,17 +5013,17 @@
2378
     // \4 is a backreference to the first character of a multi-character set.
2379
     var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;
2380
     while ((match = chunker.exec(formatStr))) {
2381
-        if (match[1]) {
2382
+        if (match[1]) { // a literal string inside [ ... ]
2383
             chunks.push.apply(chunks, // append
2384
             splitStringLiteral(match[1]));
2385
         }
2386
-        else if (match[2]) {
2387
+        else if (match[2]) { // non-zero formatting inside ( ... )
2388
             chunks.push({ maybe: chunkFormatString(match[2]) });
2389
         }
2390
-        else if (match[3]) {
2391
+        else if (match[3]) { // a formatting token
2392
             chunks.push({ token: match[3] });
2393
         }
2394
-        else if (match[5]) {
2395
+        else if (match[5]) { // an unenclosed literal string
2396
             chunks.push.apply(chunks, // append
2397
             splitStringLiteral(match[5]));
2398
         }
2399
@@ -4868,7 +5134,7 @@
2400
 */
2401
 function processMaybeMarkers(s) {
2402
     return s.replace(MAYBE_REGEXP, function (m0, m1) {
2403
-        if (m1.match(/[1-9]/)) {
2404
+        if (m1.match(/[1-9]/)) { // any non-zero numeric characters?
2405
             return m1;
2406
         }
2407
         else {
2408
@@ -4907,13 +5173,31 @@
2409
 
2410
 
2411
 /***/ }),
2412
-/* 48 */
2413
+/* 50 */
2414
+/***/ (function(module, exports) {
2415
+
2416
+Object.defineProperty(exports, "__esModule", { value: true });
2417
+var EventRange = /** @class */ (function () {
2418
+    function EventRange(unzonedRange, eventDef, eventInstance) {
2419
+        this.unzonedRange = unzonedRange;
2420
+        this.eventDef = eventDef;
2421
+        if (eventInstance) {
2422
+            this.eventInstance = eventInstance;
2423
+        }
2424
+    }
2425
+    return EventRange;
2426
+}());
2427
+exports.default = EventRange;
2428
+
2429
+
2430
+/***/ }),
2431
+/* 51 */
2432
 /***/ (function(module, exports, __webpack_require__) {
2433
 
2434
 Object.defineProperty(exports, "__esModule", { value: true });
2435
 var tslib_1 = __webpack_require__(2);
2436
-var Class_1 = __webpack_require__(33);
2437
-var EmitterMixin_1 = __webpack_require__(11);
2438
+var Class_1 = __webpack_require__(35);
2439
+var EmitterMixin_1 = __webpack_require__(13);
2440
 var ListenerMixin_1 = __webpack_require__(7);
2441
 var Model = /** @class */ (function (_super) {
2442
     tslib_1.__extends(Model, _super);
2443
@@ -5069,8 +5353,8 @@
2444
         var isCallingStop = false;
2445
         var onBeforeDepChange = function (depName, val, isOptional) {
2446
             queuedChangeCnt++;
2447
-            if (queuedChangeCnt === 1) {
2448
-                if (satisfyCnt === depCnt) {
2449
+            if (queuedChangeCnt === 1) { // first change to cause a "stop" ?
2450
+                if (satisfyCnt === depCnt) { // all deps previously satisfied?
2451
                     isCallingStop = true;
2452
                     stopFunc(values);
2453
                     isCallingStop = false;
2454
@@ -5078,14 +5362,14 @@
2455
             }
2456
         };
2457
         var onDepChange = function (depName, val, isOptional) {
2458
-            if (val === undefined) {
2459
+            if (val === undefined) { // unsetting a value?
2460
                 // required dependency that was previously set?
2461
                 if (!isOptional && values[depName] !== undefined) {
2462
                     satisfyCnt--;
2463
                 }
2464
                 delete values[depName];
2465
             }
2466
-            else {
2467
+            else { // setting a value?
2468
                 // required dependency that was previously unset?
2469
                 if (!isOptional && values[depName] === undefined) {
2470
                     satisfyCnt++;
2471
@@ -5093,7 +5377,7 @@
2472
                 values[depName] = val;
2473
             }
2474
             queuedChangeCnt--;
2475
-            if (!queuedChangeCnt) {
2476
+            if (!queuedChangeCnt) { // last change to cause a "start"?
2477
                 // now finally satisfied or satisfied all along?
2478
                 if (satisfyCnt === depCnt) {
2479
                     // if the stopFunc initiated another value change, ignore it.
2480
@@ -5112,7 +5396,7 @@
2481
         // listen to dependency changes
2482
         depList.forEach(function (depName) {
2483
             var isOptional = false;
2484
-            if (depName.charAt(0) === '?') {
2485
+            if (depName.charAt(0) === '?') { // TODO: more DRY
2486
                 depName = depName.substring(1);
2487
                 isOptional = true;
2488
             }
2489
@@ -5126,7 +5410,7 @@
2490
         // process current dependency values
2491
         depList.forEach(function (depName) {
2492
             var isOptional = false;
2493
-            if (depName.charAt(0) === '?') {
2494
+            if (depName.charAt(0) === '?') { // TODO: more DRY
2495
                 depName = depName.substring(1);
2496
                 isOptional = true;
2497
             }
2498
@@ -5177,8637 +5461,8408 @@
2499
 
2500
 
2501
 /***/ }),
2502
-/* 49 */
2503
+/* 52 */
2504
 /***/ (function(module, exports, __webpack_require__) {
2505
 
2506
+/*
2507
+USAGE:
2508
+  import { default as ParsableModelMixin, ParsableModelInterface } from './ParsableModelMixin'
2509
+in class:
2510
+  applyProps: ParsableModelInterface['applyProps']
2511
+  applyManualStandardProps: ParsableModelInterface['applyManualStandardProps']
2512
+  applyMiscProps: ParsableModelInterface['applyMiscProps']
2513
+  isStandardProp: ParsableModelInterface['isStandardProp']
2514
+  static defineStandardProps = ParsableModelMixin.defineStandardProps
2515
+  static copyVerbatimStandardProps = ParsableModelMixin.copyVerbatimStandardProps
2516
+after class:
2517
+  ParsableModelMixin.mixInto(TheClass)
2518
+*/
2519
 Object.defineProperty(exports, "__esModule", { value: true });
2520
-var moment = __webpack_require__(0);
2521
+var tslib_1 = __webpack_require__(2);
2522
 var util_1 = __webpack_require__(4);
2523
-var SingleEventDef_1 = __webpack_require__(13);
2524
-var RecurringEventDef_1 = __webpack_require__(210);
2525
-exports.default = {
2526
-    parse: function (eventInput, source) {
2527
-        if (util_1.isTimeString(eventInput.start) || moment.isDuration(eventInput.start) ||
2528
-            util_1.isTimeString(eventInput.end) || moment.isDuration(eventInput.end)) {
2529
-            return RecurringEventDef_1.default.parse(eventInput, source);
2530
+var Mixin_1 = __webpack_require__(15);
2531
+var ParsableModelMixin = /** @class */ (function (_super) {
2532
+    tslib_1.__extends(ParsableModelMixin, _super);
2533
+    function ParsableModelMixin() {
2534
+        return _super !== null && _super.apply(this, arguments) || this;
2535
+    }
2536
+    ParsableModelMixin.defineStandardProps = function (propDefs) {
2537
+        var proto = this.prototype;
2538
+        if (!proto.hasOwnProperty('standardPropMap')) {
2539
+            proto.standardPropMap = Object.create(proto.standardPropMap);
2540
         }
2541
-        else {
2542
-            return SingleEventDef_1.default.parse(eventInput, source);
2543
+        util_1.copyOwnProps(propDefs, proto.standardPropMap);
2544
+    };
2545
+    ParsableModelMixin.copyVerbatimStandardProps = function (src, dest) {
2546
+        var map = this.prototype.standardPropMap;
2547
+        var propName;
2548
+        for (propName in map) {
2549
+            if (src[propName] != null && // in the src object?
2550
+                map[propName] === true // false means "copy verbatim"
2551
+            ) {
2552
+                dest[propName] = src[propName];
2553
+            }
2554
         }
2555
-    }
2556
-};
2557
-
2558
-
2559
-/***/ }),
2560
-/* 50 */
2561
-/***/ (function(module, exports, __webpack_require__) {
2562
-
2563
-Object.defineProperty(exports, "__esModule", { value: true });
2564
-var util_1 = __webpack_require__(4);
2565
-var EventDateProfile_1 = __webpack_require__(17);
2566
-var EventDefDateMutation = /** @class */ (function () {
2567
-    function EventDefDateMutation() {
2568
-        this.clearEnd = false;
2569
-        this.forceTimed = false;
2570
-        this.forceAllDay = false;
2571
-    }
2572
-    EventDefDateMutation.createFromDiff = function (dateProfile0, dateProfile1, largeUnit) {
2573
-        var clearEnd = dateProfile0.end && !dateProfile1.end;
2574
-        var forceTimed = dateProfile0.isAllDay() && !dateProfile1.isAllDay();
2575
-        var forceAllDay = !dateProfile0.isAllDay() && dateProfile1.isAllDay();
2576
-        var dateDelta;
2577
-        var endDiff;
2578
-        var endDelta;
2579
-        var mutation;
2580
-        // subtracts the dates in the appropriate way, returning a duration
2581
-        function subtractDates(date1, date0) {
2582
-            if (largeUnit) {
2583
-                return util_1.diffByUnit(date1, date0, largeUnit); // poorly named
2584
-            }
2585
-            else if (dateProfile1.isAllDay()) {
2586
-                return util_1.diffDay(date1, date0); // poorly named
2587
-            }
2588
-            else {
2589
-                return util_1.diffDayTime(date1, date0); // poorly named
2590
-            }
2591
-        }
2592
-        dateDelta = subtractDates(dateProfile1.start, dateProfile0.start);
2593
-        if (dateProfile1.end) {
2594
-            // use unzonedRanges because dateProfile0.end might be null
2595
-            endDiff = subtractDates(dateProfile1.unzonedRange.getEnd(), dateProfile0.unzonedRange.getEnd());
2596
-            endDelta = endDiff.subtract(dateDelta);
2597
-        }
2598
-        mutation = new EventDefDateMutation();
2599
-        mutation.clearEnd = clearEnd;
2600
-        mutation.forceTimed = forceTimed;
2601
-        mutation.forceAllDay = forceAllDay;
2602
-        mutation.setDateDelta(dateDelta);
2603
-        mutation.setEndDelta(endDelta);
2604
-        return mutation;
2605
     };
2606
     /*
2607
-    returns an undo function.
2608
+    Returns true/false for success.
2609
+    Meant to be only called ONCE, at object creation.
2610
     */
2611
-    EventDefDateMutation.prototype.buildNewDateProfile = function (eventDateProfile, calendar) {
2612
-        var start = eventDateProfile.start.clone();
2613
-        var end = null;
2614
-        var shouldRezone = false;
2615
-        if (eventDateProfile.end && !this.clearEnd) {
2616
-            end = eventDateProfile.end.clone();
2617
-        }
2618
-        else if (this.endDelta && !end) {
2619
-            end = calendar.getDefaultEventEnd(eventDateProfile.isAllDay(), start);
2620
-        }
2621
-        if (this.forceTimed) {
2622
-            shouldRezone = true;
2623
-            if (!start.hasTime()) {
2624
-                start.time(0);
2625
-            }
2626
-            if (end && !end.hasTime()) {
2627
-                end.time(0);
2628
-            }
2629
-        }
2630
-        else if (this.forceAllDay) {
2631
-            if (start.hasTime()) {
2632
-                start.stripTime();
2633
-            }
2634
-            if (end && end.hasTime()) {
2635
-                end.stripTime();
2636
+    ParsableModelMixin.prototype.applyProps = function (rawProps) {
2637
+        var standardPropMap = this.standardPropMap;
2638
+        var manualProps = {};
2639
+        var miscProps = {};
2640
+        var propName;
2641
+        for (propName in rawProps) {
2642
+            if (standardPropMap[propName] === true) { // copy verbatim
2643
+                this[propName] = rawProps[propName];
2644
             }
2645
-        }
2646
-        if (this.dateDelta) {
2647
-            shouldRezone = true;
2648
-            start.add(this.dateDelta);
2649
-            if (end) {
2650
-                end.add(this.dateDelta);
2651
+            else if (standardPropMap[propName] === false) {
2652
+                manualProps[propName] = rawProps[propName];
2653
             }
2654
-        }
2655
-        // do this before adding startDelta to start, so we can work off of start
2656
-        if (this.endDelta) {
2657
-            shouldRezone = true;
2658
-            end.add(this.endDelta);
2659
-        }
2660
-        if (this.startDelta) {
2661
-            shouldRezone = true;
2662
-            start.add(this.startDelta);
2663
-        }
2664
-        if (shouldRezone) {
2665
-            start = calendar.applyTimezone(start);
2666
-            if (end) {
2667
-                end = calendar.applyTimezone(end);
2668
+            else {
2669
+                miscProps[propName] = rawProps[propName];
2670
             }
2671
         }
2672
-        // TODO: okay to access calendar option?
2673
-        if (!end && calendar.opt('forceEventDuration')) {
2674
-            end = calendar.getDefaultEventEnd(eventDateProfile.isAllDay(), start);
2675
-        }
2676
-        return new EventDateProfile_1.default(start, end, calendar);
2677
-    };
2678
-    EventDefDateMutation.prototype.setDateDelta = function (dateDelta) {
2679
-        if (dateDelta && dateDelta.valueOf()) {
2680
-            this.dateDelta = dateDelta;
2681
-        }
2682
-        else {
2683
-            this.dateDelta = null;
2684
-        }
2685
+        this.applyMiscProps(miscProps);
2686
+        return this.applyManualStandardProps(manualProps);
2687
     };
2688
-    EventDefDateMutation.prototype.setStartDelta = function (startDelta) {
2689
-        if (startDelta && startDelta.valueOf()) {
2690
-            this.startDelta = startDelta;
2691
-        }
2692
-        else {
2693
-            this.startDelta = null;
2694
-        }
2695
+    /*
2696
+    If subclasses override, they must call this supermethod and return the boolean response.
2697
+    Meant to be only called ONCE, at object creation.
2698
+    */
2699
+    ParsableModelMixin.prototype.applyManualStandardProps = function (rawProps) {
2700
+        return true;
2701
     };
2702
-    EventDefDateMutation.prototype.setEndDelta = function (endDelta) {
2703
-        if (endDelta && endDelta.valueOf()) {
2704
-            this.endDelta = endDelta;
2705
-        }
2706
-        else {
2707
-            this.endDelta = null;
2708
-        }
2709
+    /*
2710
+    Can be called even after initial object creation.
2711
+    */
2712
+    ParsableModelMixin.prototype.applyMiscProps = function (rawProps) {
2713
+        // subclasses can implement
2714
     };
2715
-    EventDefDateMutation.prototype.isEmpty = function () {
2716
-        return !this.clearEnd && !this.forceTimed && !this.forceAllDay &&
2717
-            !this.dateDelta && !this.startDelta && !this.endDelta;
2718
+    /*
2719
+    TODO: why is this a method when defineStandardProps is static
2720
+    */
2721
+    ParsableModelMixin.prototype.isStandardProp = function (propName) {
2722
+        return propName in this.standardPropMap;
2723
     };
2724
-    return EventDefDateMutation;
2725
-}());
2726
-exports.default = EventDefDateMutation;
2727
+    return ParsableModelMixin;
2728
+}(Mixin_1.default));
2729
+exports.default = ParsableModelMixin;
2730
+ParsableModelMixin.prototype.standardPropMap = {}; // will be cloned by defineStandardProps
2731
 
2732
 
2733
 /***/ }),
2734
-/* 51 */
2735
-/***/ (function(module, exports, __webpack_require__) {
2736
+/* 53 */
2737
+/***/ (function(module, exports) {
2738
 
2739
 Object.defineProperty(exports, "__esModule", { value: true });
2740
-var StandardTheme_1 = __webpack_require__(213);
2741
-var JqueryUiTheme_1 = __webpack_require__(214);
2742
-var themeClassHash = {};
2743
-function defineThemeSystem(themeName, themeClass) {
2744
-    themeClassHash[themeName] = themeClass;
2745
-}
2746
-exports.defineThemeSystem = defineThemeSystem;
2747
-function getThemeSystemClass(themeSetting) {
2748
-    if (!themeSetting) {
2749
-        return StandardTheme_1.default;
2750
-    }
2751
-    else if (themeSetting === true) {
2752
-        return JqueryUiTheme_1.default;
2753
-    }
2754
-    else {
2755
-        return themeClassHash[themeSetting];
2756
+var EventInstance = /** @class */ (function () {
2757
+    function EventInstance(def, dateProfile) {
2758
+        this.def = def;
2759
+        this.dateProfile = dateProfile;
2760
     }
2761
-}
2762
-exports.getThemeSystemClass = getThemeSystemClass;
2763
+    EventInstance.prototype.toLegacy = function () {
2764
+        var dateProfile = this.dateProfile;
2765
+        var obj = this.def.toLegacy();
2766
+        obj.start = dateProfile.start.clone();
2767
+        obj.end = dateProfile.end ? dateProfile.end.clone() : null;
2768
+        return obj;
2769
+    };
2770
+    return EventInstance;
2771
+}());
2772
+exports.default = EventInstance;
2773
 
2774
 
2775
 /***/ }),
2776
-/* 52 */
2777
+/* 54 */
2778
 /***/ (function(module, exports, __webpack_require__) {
2779
 
2780
 Object.defineProperty(exports, "__esModule", { value: true });
2781
 var tslib_1 = __webpack_require__(2);
2782
 var $ = __webpack_require__(3);
2783
-var util_1 = __webpack_require__(4);
2784
-var Promise_1 = __webpack_require__(20);
2785
-var EventSource_1 = __webpack_require__(6);
2786
-var SingleEventDef_1 = __webpack_require__(13);
2787
-var ArrayEventSource = /** @class */ (function (_super) {
2788
-    tslib_1.__extends(ArrayEventSource, _super);
2789
-    function ArrayEventSource(calendar) {
2790
-        var _this = _super.call(this, calendar) || this;
2791
-        _this.eventDefs = []; // for if setRawEventDefs is never called
2792
-        return _this;
2793
+var moment = __webpack_require__(0);
2794
+var EventDef_1 = __webpack_require__(37);
2795
+var EventInstance_1 = __webpack_require__(53);
2796
+var EventDateProfile_1 = __webpack_require__(16);
2797
+var RecurringEventDef = /** @class */ (function (_super) {
2798
+    tslib_1.__extends(RecurringEventDef, _super);
2799
+    function RecurringEventDef() {
2800
+        return _super !== null && _super.apply(this, arguments) || this;
2801
     }
2802
-    ArrayEventSource.parse = function (rawInput, calendar) {
2803
-        var rawProps;
2804
-        // normalize raw input
2805
-        if ($.isArray(rawInput.events)) {
2806
-            rawProps = rawInput;
2807
+    RecurringEventDef.prototype.isAllDay = function () {
2808
+        return !this.startTime && !this.endTime;
2809
+    };
2810
+    RecurringEventDef.prototype.buildInstances = function (unzonedRange) {
2811
+        var calendar = this.source.calendar;
2812
+        var unzonedDate = unzonedRange.getStart();
2813
+        var unzonedEnd = unzonedRange.getEnd();
2814
+        var zonedDayStart;
2815
+        var instanceStart;
2816
+        var instanceEnd;
2817
+        var instances = [];
2818
+        while (unzonedDate.isBefore(unzonedEnd)) {
2819
+            // if everyday, or this particular day-of-week
2820
+            if (!this.dowHash || this.dowHash[unzonedDate.day()]) {
2821
+                zonedDayStart = calendar.applyTimezone(unzonedDate);
2822
+                instanceStart = zonedDayStart.clone();
2823
+                instanceEnd = null;
2824
+                if (this.startTime) {
2825
+                    instanceStart.time(this.startTime);
2826
+                }
2827
+                else {
2828
+                    instanceStart.stripTime();
2829
+                }
2830
+                if (this.endTime) {
2831
+                    instanceEnd = zonedDayStart.clone().time(this.endTime);
2832
+                }
2833
+                instances.push(new EventInstance_1.default(this, // definition
2834
+                new EventDateProfile_1.default(instanceStart, instanceEnd, calendar)));
2835
+            }
2836
+            unzonedDate.add(1, 'days');
2837
         }
2838
-        else if ($.isArray(rawInput)) {
2839
-            rawProps = { events: rawInput };
2840
+        return instances;
2841
+    };
2842
+    RecurringEventDef.prototype.setDow = function (dowNumbers) {
2843
+        if (!this.dowHash) {
2844
+            this.dowHash = {};
2845
         }
2846
-        if (rawProps) {
2847
-            return EventSource_1.default.parse.call(this, rawProps, calendar);
2848
+        for (var i = 0; i < dowNumbers.length; i++) {
2849
+            this.dowHash[dowNumbers[i]] = true;
2850
         }
2851
-        return false;
2852
-    };
2853
-    ArrayEventSource.prototype.setRawEventDefs = function (rawEventDefs) {
2854
-        this.rawEventDefs = rawEventDefs;
2855
-        this.eventDefs = this.parseEventDefs(rawEventDefs);
2856
     };
2857
-    ArrayEventSource.prototype.fetch = function (start, end, timezone) {
2858
-        var eventDefs = this.eventDefs;
2859
-        var i;
2860
-        if (this.currentTimezone != null &&
2861
-            this.currentTimezone !== timezone) {
2862
-            for (i = 0; i < eventDefs.length; i++) {
2863
-                if (eventDefs[i] instanceof SingleEventDef_1.default) {
2864
-                    eventDefs[i].rezone();
2865
-                }
2866
-            }
2867
+    RecurringEventDef.prototype.clone = function () {
2868
+        var def = _super.prototype.clone.call(this);
2869
+        if (def.startTime) {
2870
+            def.startTime = moment.duration(this.startTime);
2871
         }
2872
-        this.currentTimezone = timezone;
2873
-        return Promise_1.default.resolve(eventDefs);
2874
-    };
2875
-    ArrayEventSource.prototype.addEventDef = function (eventDef) {
2876
-        this.eventDefs.push(eventDef);
2877
-    };
2878
-    /*
2879
-    eventDefId already normalized to a string
2880
-    */
2881
-    ArrayEventSource.prototype.removeEventDefsById = function (eventDefId) {
2882
-        return util_1.removeMatching(this.eventDefs, function (eventDef) {
2883
-            return eventDef.id === eventDefId;
2884
-        });
2885
-    };
2886
-    ArrayEventSource.prototype.removeAllEventDefs = function () {
2887
-        this.eventDefs = [];
2888
-    };
2889
-    ArrayEventSource.prototype.getPrimitive = function () {
2890
-        return this.rawEventDefs;
2891
-    };
2892
-    ArrayEventSource.prototype.applyManualStandardProps = function (rawProps) {
2893
-        var superSuccess = _super.prototype.applyManualStandardProps.call(this, rawProps);
2894
-        this.setRawEventDefs(rawProps.events);
2895
-        return superSuccess;
2896
+        if (def.endTime) {
2897
+            def.endTime = moment.duration(this.endTime);
2898
+        }
2899
+        if (this.dowHash) {
2900
+            def.dowHash = $.extend({}, this.dowHash);
2901
+        }
2902
+        return def;
2903
     };
2904
-    return ArrayEventSource;
2905
-}(EventSource_1.default));
2906
-exports.default = ArrayEventSource;
2907
-ArrayEventSource.defineStandardProps({
2908
-    events: false // don't automatically transfer
2909
+    return RecurringEventDef;
2910
+}(EventDef_1.default));
2911
+exports.default = RecurringEventDef;
2912
+/*
2913
+HACK to work with TypeScript mixins
2914
+NOTE: if super-method fails, should still attempt to apply
2915
+*/
2916
+RecurringEventDef.prototype.applyProps = function (rawProps) {
2917
+    var superSuccess = EventDef_1.default.prototype.applyProps.call(this, rawProps);
2918
+    if (rawProps.start) {
2919
+        this.startTime = moment.duration(rawProps.start);
2920
+    }
2921
+    if (rawProps.end) {
2922
+        this.endTime = moment.duration(rawProps.end);
2923
+    }
2924
+    if (rawProps.dow) {
2925
+        this.setDow(rawProps.dow);
2926
+    }
2927
+    return superSuccess;
2928
+};
2929
+// Parsing
2930
+// ---------------------------------------------------------------------------------------------------------------------
2931
+RecurringEventDef.defineStandardProps({
2932
+    start: false,
2933
+    end: false,
2934
+    dow: false
2935
 });
2936
 
2937
 
2938
 /***/ }),
2939
-/* 53 */
2940
+/* 55 */
2941
 /***/ (function(module, exports, __webpack_require__) {
2942
 
2943
 Object.defineProperty(exports, "__esModule", { value: true });
2944
-var $ = __webpack_require__(3);
2945
+var moment = __webpack_require__(0);
2946
 var util_1 = __webpack_require__(4);
2947
-/*
2948
-A cache for the left/right/top/bottom/width/height values for one or more elements.
2949
-Works with both offset (from topleft document) and position (from offsetParent).
2950
-
2951
-options:
2952
-- els
2953
-- isHorizontal
2954
-- isVertical
2955
-*/
2956
-var CoordCache = /** @class */ (function () {
2957
-    function CoordCache(options) {
2958
-        this.isHorizontal = false; // whether to query for left/right/width
2959
-        this.isVertical = false; // whether to query for top/bottom/height
2960
-        this.els = $(options.els);
2961
-        this.isHorizontal = options.isHorizontal;
2962
-        this.isVertical = options.isVertical;
2963
-        this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null;
2964
+var UnzonedRange_1 = __webpack_require__(5);
2965
+var DateProfileGenerator = /** @class */ (function () {
2966
+    function DateProfileGenerator(_view) {
2967
+        this._view = _view;
2968
     }
2969
-    // Queries the els for coordinates and stores them.
2970
-    // Call this method before using and of the get* methods below.
2971
-    CoordCache.prototype.build = function () {
2972
-        var offsetParentEl = this.forcedOffsetParentEl;
2973
-        if (!offsetParentEl && this.els.length > 0) {
2974
-            offsetParentEl = this.els.eq(0).offsetParent();
2975
-        }
2976
-        this.origin = offsetParentEl ?
2977
-            offsetParentEl.offset() :
2978
-            null;
2979
-        this.boundingRect = this.queryBoundingRect();
2980
-        if (this.isHorizontal) {
2981
-            this.buildElHorizontals();
2982
-        }
2983
-        if (this.isVertical) {
2984
-            this.buildElVerticals();
2985
-        }
2986
+    DateProfileGenerator.prototype.opt = function (name) {
2987
+        return this._view.opt(name);
2988
     };
2989
-    // Destroys all internal data about coordinates, freeing memory
2990
-    CoordCache.prototype.clear = function () {
2991
-        this.origin = null;
2992
-        this.boundingRect = null;
2993
-        this.lefts = null;
2994
-        this.rights = null;
2995
-        this.tops = null;
2996
-        this.bottoms = null;
2997
+    DateProfileGenerator.prototype.trimHiddenDays = function (unzonedRange) {
2998
+        return this._view.trimHiddenDays(unzonedRange);
2999
     };
3000
-    // When called, if coord caches aren't built, builds them
3001
-    CoordCache.prototype.ensureBuilt = function () {
3002
-        if (!this.origin) {
3003
-            this.build();
3004
-        }
3005
+    DateProfileGenerator.prototype.msToUtcMoment = function (ms, forceAllDay) {
3006
+        return this._view.calendar.msToUtcMoment(ms, forceAllDay);
3007
     };
3008
-    // Populates the left/right internal coordinate arrays
3009
-    CoordCache.prototype.buildElHorizontals = function () {
3010
-        var lefts = [];
3011
-        var rights = [];
3012
-        this.els.each(function (i, node) {
3013
-            var el = $(node);
3014
-            var left = el.offset().left;
3015
-            var width = el.outerWidth();
3016
-            lefts.push(left);
3017
-            rights.push(left + width);
3018
-        });
3019
-        this.lefts = lefts;
3020
-        this.rights = rights;
3021
+    /* Date Range Computation
3022
+    ------------------------------------------------------------------------------------------------------------------*/
3023
+    // Builds a structure with info about what the dates/ranges will be for the "prev" view.
3024
+    DateProfileGenerator.prototype.buildPrev = function (currentDateProfile) {
3025
+        var prevDate = currentDateProfile.date.clone()
3026
+            .startOf(currentDateProfile.currentRangeUnit)
3027
+            .subtract(currentDateProfile.dateIncrement);
3028
+        return this.build(prevDate, -1);
3029
     };
3030
-    // Populates the top/bottom internal coordinate arrays
3031
-    CoordCache.prototype.buildElVerticals = function () {
3032
-        var tops = [];
3033
-        var bottoms = [];
3034
-        this.els.each(function (i, node) {
3035
-            var el = $(node);
3036
-            var top = el.offset().top;
3037
-            var height = el.outerHeight();
3038
-            tops.push(top);
3039
-            bottoms.push(top + height);
3040
-        });
3041
-        this.tops = tops;
3042
-        this.bottoms = bottoms;
3043
+    // Builds a structure with info about what the dates/ranges will be for the "next" view.
3044
+    DateProfileGenerator.prototype.buildNext = function (currentDateProfile) {
3045
+        var nextDate = currentDateProfile.date.clone()
3046
+            .startOf(currentDateProfile.currentRangeUnit)
3047
+            .add(currentDateProfile.dateIncrement);
3048
+        return this.build(nextDate, 1);
3049
     };
3050
-    // Given a left offset (from document left), returns the index of the el that it horizontally intersects.
3051
-    // If no intersection is made, returns undefined.
3052
-    CoordCache.prototype.getHorizontalIndex = function (leftOffset) {
3053
-        this.ensureBuilt();
3054
-        var lefts = this.lefts;
3055
-        var rights = this.rights;
3056
-        var len = lefts.length;
3057
-        var i;
3058
-        for (i = 0; i < len; i++) {
3059
-            if (leftOffset >= lefts[i] && leftOffset < rights[i]) {
3060
-                return i;
3061
-            }
3062
+    // Builds a structure holding dates/ranges for rendering around the given date.
3063
+    // Optional direction param indicates whether the date is being incremented/decremented
3064
+    // from its previous value. decremented = -1, incremented = 1 (default).
3065
+    DateProfileGenerator.prototype.build = function (date, direction, forceToValid) {
3066
+        if (forceToValid === void 0) { forceToValid = false; }
3067
+        var isDateAllDay = !date.hasTime();
3068
+        var validUnzonedRange;
3069
+        var minTime = null;
3070
+        var maxTime = null;
3071
+        var currentInfo;
3072
+        var isRangeAllDay;
3073
+        var renderUnzonedRange;
3074
+        var activeUnzonedRange;
3075
+        var isValid;
3076
+        validUnzonedRange = this.buildValidRange();
3077
+        validUnzonedRange = this.trimHiddenDays(validUnzonedRange);
3078
+        if (forceToValid) {
3079
+            date = this.msToUtcMoment(validUnzonedRange.constrainDate(date), // returns MS
3080
+            isDateAllDay);
3081
         }
3082
-    };
3083
-    // Given a top offset (from document top), returns the index of the el that it vertically intersects.
3084
-    // If no intersection is made, returns undefined.
3085
-    CoordCache.prototype.getVerticalIndex = function (topOffset) {
3086
-        this.ensureBuilt();
3087
-        var tops = this.tops;
3088
-        var bottoms = this.bottoms;
3089
-        var len = tops.length;
3090
-        var i;
3091
-        for (i = 0; i < len; i++) {
3092
-            if (topOffset >= tops[i] && topOffset < bottoms[i]) {
3093
-                return i;
3094
-            }
3095
+        currentInfo = this.buildCurrentRangeInfo(date, direction);
3096
+        isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit);
3097
+        renderUnzonedRange = this.buildRenderRange(this.trimHiddenDays(currentInfo.unzonedRange), currentInfo.unit, isRangeAllDay);
3098
+        renderUnzonedRange = this.trimHiddenDays(renderUnzonedRange);
3099
+        activeUnzonedRange = renderUnzonedRange.clone();
3100
+        if (!this.opt('showNonCurrentDates')) {
3101
+            activeUnzonedRange = activeUnzonedRange.intersect(currentInfo.unzonedRange);
3102
         }
3103
+        minTime = moment.duration(this.opt('minTime'));
3104
+        maxTime = moment.duration(this.opt('maxTime'));
3105
+        activeUnzonedRange = this.adjustActiveRange(activeUnzonedRange, minTime, maxTime);
3106
+        activeUnzonedRange = activeUnzonedRange.intersect(validUnzonedRange); // might return null
3107
+        if (activeUnzonedRange) {
3108
+            date = this.msToUtcMoment(activeUnzonedRange.constrainDate(date), // returns MS
3109
+            isDateAllDay);
3110
+        }
3111
+        // it's invalid if the originally requested date is not contained,
3112
+        // or if the range is completely outside of the valid range.
3113
+        isValid = currentInfo.unzonedRange.intersectsWith(validUnzonedRange);
3114
+        return {
3115
+            // constraint for where prev/next operations can go and where events can be dragged/resized to.
3116
+            // an object with optional start and end properties.
3117
+            validUnzonedRange: validUnzonedRange,
3118
+            // range the view is formally responsible for.
3119
+            // for example, a month view might have 1st-31st, excluding padded dates
3120
+            currentUnzonedRange: currentInfo.unzonedRange,
3121
+            // name of largest unit being displayed, like "month" or "week"
3122
+            currentRangeUnit: currentInfo.unit,
3123
+            isRangeAllDay: isRangeAllDay,
3124
+            // dates that display events and accept drag-n-drop
3125
+            // will be `null` if no dates accept events
3126
+            activeUnzonedRange: activeUnzonedRange,
3127
+            // date range with a rendered skeleton
3128
+            // includes not-active days that need some sort of DOM
3129
+            renderUnzonedRange: renderUnzonedRange,
3130
+            // Duration object that denotes the first visible time of any given day
3131
+            minTime: minTime,
3132
+            // Duration object that denotes the exclusive visible end time of any given day
3133
+            maxTime: maxTime,
3134
+            isValid: isValid,
3135
+            date: date,
3136
+            // how far the current date will move for a prev/next operation
3137
+            dateIncrement: this.buildDateIncrement(currentInfo.duration)
3138
+            // pass a fallback (might be null) ^
3139
+        };
3140
     };
3141
-    // Gets the left offset (from document left) of the element at the given index
3142
-    CoordCache.prototype.getLeftOffset = function (leftIndex) {
3143
-        this.ensureBuilt();
3144
-        return this.lefts[leftIndex];
3145
-    };
3146
-    // Gets the left position (from offsetParent left) of the element at the given index
3147
-    CoordCache.prototype.getLeftPosition = function (leftIndex) {
3148
-        this.ensureBuilt();
3149
-        return this.lefts[leftIndex] - this.origin.left;
3150
-    };
3151
-    // Gets the right offset (from document left) of the element at the given index.
3152
-    // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be.
3153
-    CoordCache.prototype.getRightOffset = function (leftIndex) {
3154
-        this.ensureBuilt();
3155
-        return this.rights[leftIndex];
3156
-    };
3157
-    // Gets the right position (from offsetParent left) of the element at the given index.
3158
-    // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be.
3159
-    CoordCache.prototype.getRightPosition = function (leftIndex) {
3160
-        this.ensureBuilt();
3161
-        return this.rights[leftIndex] - this.origin.left;
3162
-    };
3163
-    // Gets the width of the element at the given index
3164
-    CoordCache.prototype.getWidth = function (leftIndex) {
3165
-        this.ensureBuilt();
3166
-        return this.rights[leftIndex] - this.lefts[leftIndex];
3167
-    };
3168
-    // Gets the top offset (from document top) of the element at the given index
3169
-    CoordCache.prototype.getTopOffset = function (topIndex) {
3170
-        this.ensureBuilt();
3171
-        return this.tops[topIndex];
3172
-    };
3173
-    // Gets the top position (from offsetParent top) of the element at the given position
3174
-    CoordCache.prototype.getTopPosition = function (topIndex) {
3175
-        this.ensureBuilt();
3176
-        return this.tops[topIndex] - this.origin.top;
3177
+    // Builds an object with optional start/end properties.
3178
+    // Indicates the minimum/maximum dates to display.
3179
+    // not responsible for trimming hidden days.
3180
+    DateProfileGenerator.prototype.buildValidRange = function () {
3181
+        return this._view.getUnzonedRangeOption('validRange', this._view.calendar.getNow()) ||
3182
+            new UnzonedRange_1.default(); // completely open-ended
3183
     };
3184
-    // Gets the bottom offset (from the document top) of the element at the given index.
3185
-    // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
3186
-    CoordCache.prototype.getBottomOffset = function (topIndex) {
3187
-        this.ensureBuilt();
3188
-        return this.bottoms[topIndex];
3189
+    // Builds a structure with info about the "current" range, the range that is
3190
+    // highlighted as being the current month for example.
3191
+    // See build() for a description of `direction`.
3192
+    // Guaranteed to have `range` and `unit` properties. `duration` is optional.
3193
+    // TODO: accept a MS-time instead of a moment `date`?
3194
+    DateProfileGenerator.prototype.buildCurrentRangeInfo = function (date, direction) {
3195
+        var viewSpec = this._view.viewSpec;
3196
+        var duration = null;
3197
+        var unit = null;
3198
+        var unzonedRange = null;
3199
+        var dayCount;
3200
+        if (viewSpec.duration) {
3201
+            duration = viewSpec.duration;
3202
+            unit = viewSpec.durationUnit;
3203
+            unzonedRange = this.buildRangeFromDuration(date, direction, duration, unit);
3204
+        }
3205
+        else if ((dayCount = this.opt('dayCount'))) {
3206
+            unit = 'day';
3207
+            unzonedRange = this.buildRangeFromDayCount(date, direction, dayCount);
3208
+        }
3209
+        else if ((unzonedRange = this.buildCustomVisibleRange(date))) {
3210
+            unit = util_1.computeGreatestUnit(unzonedRange.getStart(), unzonedRange.getEnd());
3211
+        }
3212
+        else {
3213
+            duration = this.getFallbackDuration();
3214
+            unit = util_1.computeGreatestUnit(duration);
3215
+            unzonedRange = this.buildRangeFromDuration(date, direction, duration, unit);
3216
+        }
3217
+        return { duration: duration, unit: unit, unzonedRange: unzonedRange };
3218
     };
3219
-    // Gets the bottom position (from the offsetParent top) of the element at the given index.
3220
-    // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
3221
-    CoordCache.prototype.getBottomPosition = function (topIndex) {
3222
-        this.ensureBuilt();
3223
-        return this.bottoms[topIndex] - this.origin.top;
3224
+    DateProfileGenerator.prototype.getFallbackDuration = function () {
3225
+        return moment.duration({ days: 1 });
3226
     };
3227
-    // Gets the height of the element at the given index
3228
-    CoordCache.prototype.getHeight = function (topIndex) {
3229
-        this.ensureBuilt();
3230
-        return this.bottoms[topIndex] - this.tops[topIndex];
3231
+    // Returns a new activeUnzonedRange to have time values (un-ambiguate)
3232
+    // minTime or maxTime causes the range to expand.
3233
+    DateProfileGenerator.prototype.adjustActiveRange = function (unzonedRange, minTime, maxTime) {
3234
+        var start = unzonedRange.getStart();
3235
+        var end = unzonedRange.getEnd();
3236
+        if (this._view.usesMinMaxTime) {
3237
+            if (minTime < 0) {
3238
+                start.time(0).add(minTime);
3239
+            }
3240
+            if (maxTime > 24 * 60 * 60 * 1000) { // beyond 24 hours?
3241
+                end.time(maxTime - (24 * 60 * 60 * 1000));
3242
+            }
3243
+        }
3244
+        return new UnzonedRange_1.default(start, end);
3245
     };
3246
-    // Bounding Rect
3247
-    // TODO: decouple this from CoordCache
3248
-    // Compute and return what the elements' bounding rectangle is, from the user's perspective.
3249
-    // Right now, only returns a rectangle if constrained by an overflow:scroll element.
3250
-    // Returns null if there are no elements
3251
-    CoordCache.prototype.queryBoundingRect = function () {
3252
-        var scrollParentEl;
3253
-        if (this.els.length > 0) {
3254
-            scrollParentEl = util_1.getScrollParent(this.els.eq(0));
3255
-            if (!scrollParentEl.is(document)) {
3256
-                return util_1.getClientRect(scrollParentEl);
3257
+    // Builds the "current" range when it is specified as an explicit duration.
3258
+    // `unit` is the already-computed computeGreatestUnit value of duration.
3259
+    // TODO: accept a MS-time instead of a moment `date`?
3260
+    DateProfileGenerator.prototype.buildRangeFromDuration = function (date, direction, duration, unit) {
3261
+        var alignment = this.opt('dateAlignment');
3262
+        var dateIncrementInput;
3263
+        var dateIncrementDuration;
3264
+        var start;
3265
+        var end;
3266
+        var res;
3267
+        // compute what the alignment should be
3268
+        if (!alignment) {
3269
+            dateIncrementInput = this.opt('dateIncrement');
3270
+            if (dateIncrementInput) {
3271
+                dateIncrementDuration = moment.duration(dateIncrementInput);
3272
+                // use the smaller of the two units
3273
+                if (dateIncrementDuration < duration) {
3274
+                    alignment = util_1.computeDurationGreatestUnit(dateIncrementDuration, dateIncrementInput);
3275
+                }
3276
+                else {
3277
+                    alignment = unit;
3278
+                }
3279
+            }
3280
+            else {
3281
+                alignment = unit;
3282
             }
3283
         }
3284
-        return null;
3285
+        // if the view displays a single day or smaller
3286
+        if (duration.as('days') <= 1) {
3287
+            if (this._view.isHiddenDay(start)) {
3288
+                start = this._view.skipHiddenDays(start, direction);
3289
+                start.startOf('day');
3290
+            }
3291
+        }
3292
+        function computeRes() {
3293
+            start = date.clone().startOf(alignment);
3294
+            end = start.clone().add(duration);
3295
+            res = new UnzonedRange_1.default(start, end);
3296
+        }
3297
+        computeRes();
3298
+        // if range is completely enveloped by hidden days, go past the hidden days
3299
+        if (!this.trimHiddenDays(res)) {
3300
+            date = this._view.skipHiddenDays(date, direction);
3301
+            computeRes();
3302
+        }
3303
+        return res;
3304
     };
3305
-    CoordCache.prototype.isPointInBounds = function (leftOffset, topOffset) {
3306
-        return this.isLeftInBounds(leftOffset) && this.isTopInBounds(topOffset);
3307
+    // Builds the "current" range when a dayCount is specified.
3308
+    // TODO: accept a MS-time instead of a moment `date`?
3309
+    DateProfileGenerator.prototype.buildRangeFromDayCount = function (date, direction, dayCount) {
3310
+        var customAlignment = this.opt('dateAlignment');
3311
+        var runningCount = 0;
3312
+        var start;
3313
+        var end;
3314
+        if (customAlignment || direction !== -1) {
3315
+            start = date.clone();
3316
+            if (customAlignment) {
3317
+                start.startOf(customAlignment);
3318
+            }
3319
+            start.startOf('day');
3320
+            start = this._view.skipHiddenDays(start);
3321
+            end = start.clone();
3322
+            do {
3323
+                end.add(1, 'day');
3324
+                if (!this._view.isHiddenDay(end)) {
3325
+                    runningCount++;
3326
+                }
3327
+            } while (runningCount < dayCount);
3328
+        }
3329
+        else {
3330
+            end = date.clone().startOf('day').add(1, 'day');
3331
+            end = this._view.skipHiddenDays(end, -1, true);
3332
+            start = end.clone();
3333
+            do {
3334
+                start.add(-1, 'day');
3335
+                if (!this._view.isHiddenDay(start)) {
3336
+                    runningCount++;
3337
+                }
3338
+            } while (runningCount < dayCount);
3339
+        }
3340
+        return new UnzonedRange_1.default(start, end);
3341
     };
3342
-    CoordCache.prototype.isLeftInBounds = function (leftOffset) {
3343
-        return !this.boundingRect || (leftOffset >= this.boundingRect.left && leftOffset < this.boundingRect.right);
3344
+    // Builds a normalized range object for the "visible" range,
3345
+    // which is a way to define the currentUnzonedRange and activeUnzonedRange at the same time.
3346
+    // TODO: accept a MS-time instead of a moment `date`?
3347
+    DateProfileGenerator.prototype.buildCustomVisibleRange = function (date) {
3348
+        var visibleUnzonedRange = this._view.getUnzonedRangeOption('visibleRange', this._view.calendar.applyTimezone(date) // correct zone. also generates new obj that avoids mutations
3349
+        );
3350
+        if (visibleUnzonedRange && (visibleUnzonedRange.startMs == null || visibleUnzonedRange.endMs == null)) {
3351
+            return null;
3352
+        }
3353
+        return visibleUnzonedRange;
3354
     };
3355
-    CoordCache.prototype.isTopInBounds = function (topOffset) {
3356
-        return !this.boundingRect || (topOffset >= this.boundingRect.top && topOffset < this.boundingRect.bottom);
3357
+    // Computes the range that will represent the element/cells for *rendering*,
3358
+    // but which may have voided days/times.
3359
+    // not responsible for trimming hidden days.
3360
+    DateProfileGenerator.prototype.buildRenderRange = function (currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
3361
+        return currentUnzonedRange.clone();
3362
     };
3363
-    return CoordCache;
3364
+    // Compute the duration value that should be added/substracted to the current date
3365
+    // when a prev/next operation happens.
3366
+    DateProfileGenerator.prototype.buildDateIncrement = function (fallback) {
3367
+        var dateIncrementInput = this.opt('dateIncrement');
3368
+        var customAlignment;
3369
+        if (dateIncrementInput) {
3370
+            return moment.duration(dateIncrementInput);
3371
+        }
3372
+        else if ((customAlignment = this.opt('dateAlignment'))) {
3373
+            return moment.duration(1, customAlignment);
3374
+        }
3375
+        else if (fallback) {
3376
+            return fallback;
3377
+        }
3378
+        else {
3379
+            return moment.duration({ days: 1 });
3380
+        }
3381
+    };
3382
+    return DateProfileGenerator;
3383
 }());
3384
-exports.default = CoordCache;
3385
+exports.default = DateProfileGenerator;
3386
 
3387
 
3388
 /***/ }),
3389
-/* 54 */
3390
+/* 56 */
3391
 /***/ (function(module, exports, __webpack_require__) {
3392
 
3393
 Object.defineProperty(exports, "__esModule", { value: true });
3394
+var tslib_1 = __webpack_require__(2);
3395
 var $ = __webpack_require__(3);
3396
 var util_1 = __webpack_require__(4);
3397
-var ListenerMixin_1 = __webpack_require__(7);
3398
-var GlobalEmitter_1 = __webpack_require__(21);
3399
-/* Tracks a drag's mouse movement, firing various handlers
3400
-----------------------------------------------------------------------------------------------------------------------*/
3401
-// TODO: use Emitter
3402
-var DragListener = /** @class */ (function () {
3403
-    function DragListener(options) {
3404
-        this.isInteracting = false;
3405
-        this.isDistanceSurpassed = false;
3406
-        this.isDelayEnded = false;
3407
-        this.isDragging = false;
3408
-        this.isTouch = false;
3409
-        this.isGeneric = false; // initiated by 'dragstart' (jqui)
3410
-        this.shouldCancelTouchScroll = true;
3411
-        this.scrollAlwaysKills = false;
3412
-        this.isAutoScroll = false;
3413
-        // defaults
3414
-        this.scrollSensitivity = 30; // pixels from edge for scrolling to start
3415
-        this.scrollSpeed = 200; // pixels per second, at maximum speed
3416
-        this.scrollIntervalMs = 50; // millisecond wait between scroll increment
3417
-        this.options = options || {};
3418
+var Promise_1 = __webpack_require__(21);
3419
+var EventSource_1 = __webpack_require__(6);
3420
+var SingleEventDef_1 = __webpack_require__(9);
3421
+var ArrayEventSource = /** @class */ (function (_super) {
3422
+    tslib_1.__extends(ArrayEventSource, _super);
3423
+    function ArrayEventSource(calendar) {
3424
+        var _this = _super.call(this, calendar) || this;
3425
+        _this.eventDefs = []; // for if setRawEventDefs is never called
3426
+        return _this;
3427
     }
3428
-    // Interaction (high-level)
3429
-    // -----------------------------------------------------------------------------------------------------------------
3430
-    DragListener.prototype.startInteraction = function (ev, extraOptions) {
3431
-        if (extraOptions === void 0) { extraOptions = {}; }
3432
-        if (ev.type === 'mousedown') {
3433
-            if (GlobalEmitter_1.default.get().shouldIgnoreMouse()) {
3434
-                return;
3435
-            }
3436
-            else if (!util_1.isPrimaryMouseButton(ev)) {
3437
-                return;
3438
-            }
3439
-            else {
3440
-                ev.preventDefault(); // prevents native selection in most browsers
3441
-            }
3442
+    ArrayEventSource.parse = function (rawInput, calendar) {
3443
+        var rawProps;
3444
+        // normalize raw input
3445
+        if ($.isArray(rawInput.events)) { // extended form
3446
+            rawProps = rawInput;
3447
         }
3448
-        if (!this.isInteracting) {
3449
-            // process options
3450
-            this.delay = util_1.firstDefined(extraOptions.delay, this.options.delay, 0);
3451
-            this.minDistance = util_1.firstDefined(extraOptions.distance, this.options.distance, 0);
3452
-            this.subjectEl = this.options.subjectEl;
3453
-            util_1.preventSelection($('body'));
3454
-            this.isInteracting = true;
3455
-            this.isTouch = util_1.getEvIsTouch(ev);
3456
-            this.isGeneric = ev.type === 'dragstart';
3457
-            this.isDelayEnded = false;
3458
-            this.isDistanceSurpassed = false;
3459
-            this.originX = util_1.getEvX(ev);
3460
-            this.originY = util_1.getEvY(ev);
3461
-            this.scrollEl = util_1.getScrollParent($(ev.target));
3462
-            this.bindHandlers();
3463
-            this.initAutoScroll();
3464
-            this.handleInteractionStart(ev);
3465
-            this.startDelay(ev);
3466
-            if (!this.minDistance) {
3467
-                this.handleDistanceSurpassed(ev);
3468
-            }
3469
+        else if ($.isArray(rawInput)) { // short form
3470
+            rawProps = { events: rawInput };
3471
+        }
3472
+        if (rawProps) {
3473
+            return EventSource_1.default.parse.call(this, rawProps, calendar);
3474
         }
3475
+        return false;
3476
     };
3477
-    DragListener.prototype.handleInteractionStart = function (ev) {
3478
-        this.trigger('interactionStart', ev);
3479
+    ArrayEventSource.prototype.setRawEventDefs = function (rawEventDefs) {
3480
+        this.rawEventDefs = rawEventDefs;
3481
+        this.eventDefs = this.parseEventDefs(rawEventDefs);
3482
     };
3483
-    DragListener.prototype.endInteraction = function (ev, isCancelled) {
3484
-        if (this.isInteracting) {
3485
-            this.endDrag(ev);
3486
-            if (this.delayTimeoutId) {
3487
-                clearTimeout(this.delayTimeoutId);
3488
-                this.delayTimeoutId = null;
3489
+    ArrayEventSource.prototype.fetch = function (start, end, timezone) {
3490
+        var eventDefs = this.eventDefs;
3491
+        var i;
3492
+        if (this.currentTimezone != null &&
3493
+            this.currentTimezone !== timezone) {
3494
+            for (i = 0; i < eventDefs.length; i++) {
3495
+                if (eventDefs[i] instanceof SingleEventDef_1.default) {
3496
+                    eventDefs[i].rezone();
3497
+                }
3498
             }
3499
-            this.destroyAutoScroll();
3500
-            this.unbindHandlers();
3501
-            this.isInteracting = false;
3502
-            this.handleInteractionEnd(ev, isCancelled);
3503
-            util_1.allowSelection($('body'));
3504
         }
3505
+        this.currentTimezone = timezone;
3506
+        return Promise_1.default.resolve(eventDefs);
3507
     };
3508
-    DragListener.prototype.handleInteractionEnd = function (ev, isCancelled) {
3509
-        this.trigger('interactionEnd', ev, isCancelled || false);
3510
+    ArrayEventSource.prototype.addEventDef = function (eventDef) {
3511
+        this.eventDefs.push(eventDef);
3512
     };
3513
-    // Binding To DOM
3514
-    // -----------------------------------------------------------------------------------------------------------------
3515
-    DragListener.prototype.bindHandlers = function () {
3516
-        // some browsers (Safari in iOS 10) don't allow preventDefault on touch events that are bound after touchstart,
3517
-        // so listen to the GlobalEmitter singleton, which is always bound, instead of the document directly.
3518
-        var globalEmitter = GlobalEmitter_1.default.get();
3519
-        if (this.isGeneric) {
3520
-            this.listenTo($(document), {
3521
-                drag: this.handleMove,
3522
-                dragstop: this.endInteraction
3523
-            });
3524
-        }
3525
-        else if (this.isTouch) {
3526
-            this.listenTo(globalEmitter, {
3527
-                touchmove: this.handleTouchMove,
3528
-                touchend: this.endInteraction,
3529
-                scroll: this.handleTouchScroll
3530
-            });
3531
-        }
3532
-        else {
3533
-            this.listenTo(globalEmitter, {
3534
-                mousemove: this.handleMouseMove,
3535
-                mouseup: this.endInteraction
3536
-            });
3537
-        }
3538
-        this.listenTo(globalEmitter, {
3539
-            selectstart: util_1.preventDefault,
3540
-            contextmenu: util_1.preventDefault // long taps would open menu on Chrome dev tools
3541
+    /*
3542
+    eventDefId already normalized to a string
3543
+    */
3544
+    ArrayEventSource.prototype.removeEventDefsById = function (eventDefId) {
3545
+        return util_1.removeMatching(this.eventDefs, function (eventDef) {
3546
+            return eventDef.id === eventDefId;
3547
         });
3548
     };
3549
-    DragListener.prototype.unbindHandlers = function () {
3550
-        this.stopListeningTo(GlobalEmitter_1.default.get());
3551
-        this.stopListeningTo($(document)); // for isGeneric
3552
-    };
3553
-    // Drag (high-level)
3554
-    // -----------------------------------------------------------------------------------------------------------------
3555
-    // extraOptions ignored if drag already started
3556
-    DragListener.prototype.startDrag = function (ev, extraOptions) {
3557
-        this.startInteraction(ev, extraOptions); // ensure interaction began
3558
-        if (!this.isDragging) {
3559
-            this.isDragging = true;
3560
-            this.handleDragStart(ev);
3561
-        }
3562
-    };
3563
-    DragListener.prototype.handleDragStart = function (ev) {
3564
-        this.trigger('dragStart', ev);
3565
+    ArrayEventSource.prototype.removeAllEventDefs = function () {
3566
+        this.eventDefs = [];
3567
     };
3568
-    DragListener.prototype.handleMove = function (ev) {
3569
-        var dx = util_1.getEvX(ev) - this.originX;
3570
-        var dy = util_1.getEvY(ev) - this.originY;
3571
-        var minDistance = this.minDistance;
3572
-        var distanceSq; // current distance from the origin, squared
3573
-        if (!this.isDistanceSurpassed) {
3574
-            distanceSq = dx * dx + dy * dy;
3575
-            if (distanceSq >= minDistance * minDistance) {
3576
-                this.handleDistanceSurpassed(ev);
3577
-            }
3578
-        }
3579
-        if (this.isDragging) {
3580
-            this.handleDrag(dx, dy, ev);
3581
-        }
3582
+    ArrayEventSource.prototype.getPrimitive = function () {
3583
+        return this.rawEventDefs;
3584
     };
3585
-    // Called while the mouse is being moved and when we know a legitimate drag is taking place
3586
-    DragListener.prototype.handleDrag = function (dx, dy, ev) {
3587
-        this.trigger('drag', dx, dy, ev);
3588
-        this.updateAutoScroll(ev); // will possibly cause scrolling
3589
+    ArrayEventSource.prototype.applyManualStandardProps = function (rawProps) {
3590
+        var superSuccess = _super.prototype.applyManualStandardProps.call(this, rawProps);
3591
+        this.setRawEventDefs(rawProps.events);
3592
+        return superSuccess;
3593
     };
3594
-    DragListener.prototype.endDrag = function (ev) {
3595
-        if (this.isDragging) {
3596
-            this.isDragging = false;
3597
-            this.handleDragEnd(ev);
3598
+    return ArrayEventSource;
3599
+}(EventSource_1.default));
3600
+exports.default = ArrayEventSource;
3601
+ArrayEventSource.defineStandardProps({
3602
+    events: false // don't automatically transfer
3603
+});
3604
+
3605
+
3606
+/***/ }),
3607
+/* 57 */
3608
+/***/ (function(module, exports, __webpack_require__) {
3609
+
3610
+Object.defineProperty(exports, "__esModule", { value: true });
3611
+var StandardTheme_1 = __webpack_require__(221);
3612
+var JqueryUiTheme_1 = __webpack_require__(222);
3613
+var themeClassHash = {};
3614
+function defineThemeSystem(themeName, themeClass) {
3615
+    themeClassHash[themeName] = themeClass;
3616
+}
3617
+exports.defineThemeSystem = defineThemeSystem;
3618
+function getThemeSystemClass(themeSetting) {
3619
+    if (!themeSetting) {
3620
+        return StandardTheme_1.default;
3621
+    }
3622
+    else if (themeSetting === true) {
3623
+        return JqueryUiTheme_1.default;
3624
+    }
3625
+    else {
3626
+        return themeClassHash[themeSetting];
3627
+    }
3628
+}
3629
+exports.getThemeSystemClass = getThemeSystemClass;
3630
+
3631
+
3632
+/***/ }),
3633
+/* 58 */
3634
+/***/ (function(module, exports, __webpack_require__) {
3635
+
3636
+Object.defineProperty(exports, "__esModule", { value: true });
3637
+var $ = __webpack_require__(3);
3638
+var util_1 = __webpack_require__(4);
3639
+/*
3640
+A cache for the left/right/top/bottom/width/height values for one or more elements.
3641
+Works with both offset (from topleft document) and position (from offsetParent).
3642
+
3643
+options:
3644
+- els
3645
+- isHorizontal
3646
+- isVertical
3647
+*/
3648
+var CoordCache = /** @class */ (function () {
3649
+    function CoordCache(options) {
3650
+        this.isHorizontal = false; // whether to query for left/right/width
3651
+        this.isVertical = false; // whether to query for top/bottom/height
3652
+        this.els = $(options.els);
3653
+        this.isHorizontal = options.isHorizontal;
3654
+        this.isVertical = options.isVertical;
3655
+        this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null;
3656
+    }
3657
+    // Queries the els for coordinates and stores them.
3658
+    // Call this method before using and of the get* methods below.
3659
+    CoordCache.prototype.build = function () {
3660
+        var offsetParentEl = this.forcedOffsetParentEl;
3661
+        if (!offsetParentEl && this.els.length > 0) {
3662
+            offsetParentEl = this.els.eq(0).offsetParent();
3663
         }
3664
-    };
3665
-    DragListener.prototype.handleDragEnd = function (ev) {
3666
-        this.trigger('dragEnd', ev);
3667
-    };
3668
-    // Delay
3669
-    // -----------------------------------------------------------------------------------------------------------------
3670
-    DragListener.prototype.startDelay = function (initialEv) {
3671
-        var _this = this;
3672
-        if (this.delay) {
3673
-            this.delayTimeoutId = setTimeout(function () {
3674
-                _this.handleDelayEnd(initialEv);
3675
-            }, this.delay);
3676
+        this.origin = offsetParentEl ?
3677
+            offsetParentEl.offset() :
3678
+            null;
3679
+        this.boundingRect = this.queryBoundingRect();
3680
+        if (this.isHorizontal) {
3681
+            this.buildElHorizontals();
3682
         }
3683
-        else {
3684
-            this.handleDelayEnd(initialEv);
3685
+        if (this.isVertical) {
3686
+            this.buildElVerticals();
3687
         }
3688
     };
3689
-    DragListener.prototype.handleDelayEnd = function (initialEv) {
3690
-        this.isDelayEnded = true;
3691
-        if (this.isDistanceSurpassed) {
3692
-            this.startDrag(initialEv);
3693
-        }
3694
+    // Destroys all internal data about coordinates, freeing memory
3695
+    CoordCache.prototype.clear = function () {
3696
+        this.origin = null;
3697
+        this.boundingRect = null;
3698
+        this.lefts = null;
3699
+        this.rights = null;
3700
+        this.tops = null;
3701
+        this.bottoms = null;
3702
     };
3703
-    // Distance
3704
-    // -----------------------------------------------------------------------------------------------------------------
3705
-    DragListener.prototype.handleDistanceSurpassed = function (ev) {
3706
-        this.isDistanceSurpassed = true;
3707
-        if (this.isDelayEnded) {
3708
-            this.startDrag(ev);
3709
+    // When called, if coord caches aren't built, builds them
3710
+    CoordCache.prototype.ensureBuilt = function () {
3711
+        if (!this.origin) {
3712
+            this.build();
3713
         }
3714
     };
3715
-    // Mouse / Touch
3716
-    // -----------------------------------------------------------------------------------------------------------------
3717
-    DragListener.prototype.handleTouchMove = function (ev) {
3718
-        // prevent inertia and touchmove-scrolling while dragging
3719
-        if (this.isDragging && this.shouldCancelTouchScroll) {
3720
-            ev.preventDefault();
3721
-        }
3722
-        this.handleMove(ev);
3723
+    // Populates the left/right internal coordinate arrays
3724
+    CoordCache.prototype.buildElHorizontals = function () {
3725
+        var lefts = [];
3726
+        var rights = [];
3727
+        this.els.each(function (i, node) {
3728
+            var el = $(node);
3729
+            var left = el.offset().left;
3730
+            var width = el.outerWidth();
3731
+            lefts.push(left);
3732
+            rights.push(left + width);
3733
+        });
3734
+        this.lefts = lefts;
3735
+        this.rights = rights;
3736
     };
3737
-    DragListener.prototype.handleMouseMove = function (ev) {
3738
-        this.handleMove(ev);
3739
+    // Populates the top/bottom internal coordinate arrays
3740
+    CoordCache.prototype.buildElVerticals = function () {
3741
+        var tops = [];
3742
+        var bottoms = [];
3743
+        this.els.each(function (i, node) {
3744
+            var el = $(node);
3745
+            var top = el.offset().top;
3746
+            var height = el.outerHeight();
3747
+            tops.push(top);
3748
+            bottoms.push(top + height);
3749
+        });
3750
+        this.tops = tops;
3751
+        this.bottoms = bottoms;
3752
     };
3753
-    // Scrolling (unrelated to auto-scroll)
3754
-    // -----------------------------------------------------------------------------------------------------------------
3755
-    DragListener.prototype.handleTouchScroll = function (ev) {
3756
-        // if the drag is being initiated by touch, but a scroll happens before
3757
-        // the drag-initiating delay is over, cancel the drag
3758
-        if (!this.isDragging || this.scrollAlwaysKills) {
3759
-            this.endInteraction(ev, true); // isCancelled=true
3760
+    // Given a left offset (from document left), returns the index of the el that it horizontally intersects.
3761
+    // If no intersection is made, returns undefined.
3762
+    CoordCache.prototype.getHorizontalIndex = function (leftOffset) {
3763
+        this.ensureBuilt();
3764
+        var lefts = this.lefts;
3765
+        var rights = this.rights;
3766
+        var len = lefts.length;
3767
+        var i;
3768
+        for (i = 0; i < len; i++) {
3769
+            if (leftOffset >= lefts[i] && leftOffset < rights[i]) {
3770
+                return i;
3771
+            }
3772
         }
3773
     };
3774
-    // Utils
3775
-    // -----------------------------------------------------------------------------------------------------------------
3776
-    // Triggers a callback. Calls a function in the option hash of the same name.
3777
-    // Arguments beyond the first `name` are forwarded on.
3778
-    DragListener.prototype.trigger = function (name) {
3779
-        var args = [];
3780
-        for (var _i = 1; _i < arguments.length; _i++) {
3781
-            args[_i - 1] = arguments[_i];
3782
-        }
3783
-        if (this.options[name]) {
3784
-            this.options[name].apply(this, args);
3785
-        }
3786
-        // makes _methods callable by event name. TODO: kill this
3787
-        if (this['_' + name]) {
3788
-            this['_' + name].apply(this, args);
3789
+    // Given a top offset (from document top), returns the index of the el that it vertically intersects.
3790
+    // If no intersection is made, returns undefined.
3791
+    CoordCache.prototype.getVerticalIndex = function (topOffset) {
3792
+        this.ensureBuilt();
3793
+        var tops = this.tops;
3794
+        var bottoms = this.bottoms;
3795
+        var len = tops.length;
3796
+        var i;
3797
+        for (i = 0; i < len; i++) {
3798
+            if (topOffset >= tops[i] && topOffset < bottoms[i]) {
3799
+                return i;
3800
+            }
3801
         }
3802
     };
3803
-    // Auto-scroll
3804
-    // -----------------------------------------------------------------------------------------------------------------
3805
-    DragListener.prototype.initAutoScroll = function () {
3806
-        var scrollEl = this.scrollEl;
3807
-        this.isAutoScroll =
3808
-            this.options.scroll &&
3809
-                scrollEl &&
3810
-                !scrollEl.is(window) &&
3811
-                !scrollEl.is(document);
3812
-        if (this.isAutoScroll) {
3813
-            // debounce makes sure rapid calls don't happen
3814
-            this.listenTo(scrollEl, 'scroll', util_1.debounce(this.handleDebouncedScroll, 100));
3815
-        }
3816
+    // Gets the left offset (from document left) of the element at the given index
3817
+    CoordCache.prototype.getLeftOffset = function (leftIndex) {
3818
+        this.ensureBuilt();
3819
+        return this.lefts[leftIndex];
3820
     };
3821
-    DragListener.prototype.destroyAutoScroll = function () {
3822
-        this.endAutoScroll(); // kill any animation loop
3823
-        // remove the scroll handler if there is a scrollEl
3824
-        if (this.isAutoScroll) {
3825
-            this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :(
3826
-        }
3827
+    // Gets the left position (from offsetParent left) of the element at the given index
3828
+    CoordCache.prototype.getLeftPosition = function (leftIndex) {
3829
+        this.ensureBuilt();
3830
+        return this.lefts[leftIndex] - this.origin.left;
3831
     };
3832
-    // Computes and stores the bounding rectangle of scrollEl
3833
-    DragListener.prototype.computeScrollBounds = function () {
3834
-        if (this.isAutoScroll) {
3835
-            this.scrollBounds = util_1.getOuterRect(this.scrollEl);
3836
-            // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
3837
-        }
3838
+    // Gets the right offset (from document left) of the element at the given index.
3839
+    // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be.
3840
+    CoordCache.prototype.getRightOffset = function (leftIndex) {
3841
+        this.ensureBuilt();
3842
+        return this.rights[leftIndex];
3843
     };
3844
-    // Called when the dragging is in progress and scrolling should be updated
3845
-    DragListener.prototype.updateAutoScroll = function (ev) {
3846
-        var sensitivity = this.scrollSensitivity;
3847
-        var bounds = this.scrollBounds;
3848
-        var topCloseness;
3849
-        var bottomCloseness;
3850
-        var leftCloseness;
3851
-        var rightCloseness;
3852
-        var topVel = 0;
3853
-        var leftVel = 0;
3854
-        if (bounds) {
3855
-            // compute closeness to edges. valid range is from 0.0 - 1.0
3856
-            topCloseness = (sensitivity - (util_1.getEvY(ev) - bounds.top)) / sensitivity;
3857
-            bottomCloseness = (sensitivity - (bounds.bottom - util_1.getEvY(ev))) / sensitivity;
3858
-            leftCloseness = (sensitivity - (util_1.getEvX(ev) - bounds.left)) / sensitivity;
3859
-            rightCloseness = (sensitivity - (bounds.right - util_1.getEvX(ev))) / sensitivity;
3860
-            // translate vertical closeness into velocity.
3861
-            // mouse must be completely in bounds for velocity to happen.
3862
-            if (topCloseness >= 0 && topCloseness <= 1) {
3863
-                topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
3864
-            }
3865
-            else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
3866
-                topVel = bottomCloseness * this.scrollSpeed;
3867
-            }
3868
-            // translate horizontal closeness into velocity
3869
-            if (leftCloseness >= 0 && leftCloseness <= 1) {
3870
-                leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
3871
-            }
3872
-            else if (rightCloseness >= 0 && rightCloseness <= 1) {
3873
-                leftVel = rightCloseness * this.scrollSpeed;
3874
-            }
3875
-        }
3876
-        this.setScrollVel(topVel, leftVel);
3877
+    // Gets the right position (from offsetParent left) of the element at the given index.
3878
+    // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be.
3879
+    CoordCache.prototype.getRightPosition = function (leftIndex) {
3880
+        this.ensureBuilt();
3881
+        return this.rights[leftIndex] - this.origin.left;
3882
     };
3883
-    // Sets the speed-of-scrolling for the scrollEl
3884
-    DragListener.prototype.setScrollVel = function (topVel, leftVel) {
3885
-        this.scrollTopVel = topVel;
3886
-        this.scrollLeftVel = leftVel;
3887
-        this.constrainScrollVel(); // massages into realistic values
3888
-        // if there is non-zero velocity, and an animation loop hasn't already started, then START
3889
-        if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
3890
-            this.scrollIntervalId = setInterval(util_1.proxy(this, 'scrollIntervalFunc'), // scope to `this`
3891
-            this.scrollIntervalMs);
3892
-        }
3893
+    // Gets the width of the element at the given index
3894
+    CoordCache.prototype.getWidth = function (leftIndex) {
3895
+        this.ensureBuilt();
3896
+        return this.rights[leftIndex] - this.lefts[leftIndex];
3897
     };
3898
-    // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
3899
-    DragListener.prototype.constrainScrollVel = function () {
3900
-        var el = this.scrollEl;
3901
-        if (this.scrollTopVel < 0) {
3902
-            if (el.scrollTop() <= 0) {
3903
-                this.scrollTopVel = 0;
3904
-            }
3905
-        }
3906
-        else if (this.scrollTopVel > 0) {
3907
-            if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) {
3908
-                this.scrollTopVel = 0;
3909
-            }
3910
-        }
3911
-        if (this.scrollLeftVel < 0) {
3912
-            if (el.scrollLeft() <= 0) {
3913
-                this.scrollLeftVel = 0;
3914
-            }
3915
-        }
3916
-        else if (this.scrollLeftVel > 0) {
3917
-            if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) {
3918
-                this.scrollLeftVel = 0;
3919
-            }
3920
-        }
3921
+    // Gets the top offset (from document top) of the element at the given index
3922
+    CoordCache.prototype.getTopOffset = function (topIndex) {
3923
+        this.ensureBuilt();
3924
+        return this.tops[topIndex];
3925
     };
3926
-    // This function gets called during every iteration of the scrolling animation loop
3927
-    DragListener.prototype.scrollIntervalFunc = function () {
3928
-        var el = this.scrollEl;
3929
-        var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
3930
-        // change the value of scrollEl's scroll
3931
-        if (this.scrollTopVel) {
3932
-            el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
3933
-        }
3934
-        if (this.scrollLeftVel) {
3935
-            el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
3936
-        }
3937
-        this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
3938
-        // if scrolled all the way, which causes the vels to be zero, stop the animation loop
3939
-        if (!this.scrollTopVel && !this.scrollLeftVel) {
3940
-            this.endAutoScroll();
3941
-        }
3942
+    // Gets the top position (from offsetParent top) of the element at the given position
3943
+    CoordCache.prototype.getTopPosition = function (topIndex) {
3944
+        this.ensureBuilt();
3945
+        return this.tops[topIndex] - this.origin.top;
3946
     };
3947
-    // Kills any existing scrolling animation loop
3948
-    DragListener.prototype.endAutoScroll = function () {
3949
-        if (this.scrollIntervalId) {
3950
-            clearInterval(this.scrollIntervalId);
3951
-            this.scrollIntervalId = null;
3952
-            this.handleScrollEnd();
3953
-        }
3954
+    // Gets the bottom offset (from the document top) of the element at the given index.
3955
+    // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
3956
+    CoordCache.prototype.getBottomOffset = function (topIndex) {
3957
+        this.ensureBuilt();
3958
+        return this.bottoms[topIndex];
3959
     };
3960
-    // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
3961
-    DragListener.prototype.handleDebouncedScroll = function () {
3962
-        // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
3963
-        if (!this.scrollIntervalId) {
3964
-            this.handleScrollEnd();
3965
+    // Gets the bottom position (from the offsetParent top) of the element at the given index.
3966
+    // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
3967
+    CoordCache.prototype.getBottomPosition = function (topIndex) {
3968
+        this.ensureBuilt();
3969
+        return this.bottoms[topIndex] - this.origin.top;
3970
+    };
3971
+    // Gets the height of the element at the given index
3972
+    CoordCache.prototype.getHeight = function (topIndex) {
3973
+        this.ensureBuilt();
3974
+        return this.bottoms[topIndex] - this.tops[topIndex];
3975
+    };
3976
+    // Bounding Rect
3977
+    // TODO: decouple this from CoordCache
3978
+    // Compute and return what the elements' bounding rectangle is, from the user's perspective.
3979
+    // Right now, only returns a rectangle if constrained by an overflow:scroll element.
3980
+    // Returns null if there are no elements
3981
+    CoordCache.prototype.queryBoundingRect = function () {
3982
+        var scrollParentEl;
3983
+        if (this.els.length > 0) {
3984
+            scrollParentEl = util_1.getScrollParent(this.els.eq(0));
3985
+            if (!scrollParentEl.is(document) &&
3986
+                !scrollParentEl.is('html,body') // don't consider these bounding rects. solves issue 3615
3987
+            ) {
3988
+                return util_1.getClientRect(scrollParentEl);
3989
+            }
3990
         }
3991
+        return null;
3992
     };
3993
-    DragListener.prototype.handleScrollEnd = function () {
3994
-        // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
3995
+    CoordCache.prototype.isPointInBounds = function (leftOffset, topOffset) {
3996
+        return this.isLeftInBounds(leftOffset) && this.isTopInBounds(topOffset);
3997
     };
3998
-    return DragListener;
3999
-}());
4000
-exports.default = DragListener;
4001
-ListenerMixin_1.default.mixInto(DragListener);
4002
+    CoordCache.prototype.isLeftInBounds = function (leftOffset) {
4003
+        return !this.boundingRect || (leftOffset >= this.boundingRect.left && leftOffset < this.boundingRect.right);
4004
+    };
4005
+    CoordCache.prototype.isTopInBounds = function (topOffset) {
4006
+        return !this.boundingRect || (topOffset >= this.boundingRect.top && topOffset < this.boundingRect.bottom);
4007
+    };
4008
+    return CoordCache;
4009
+}());
4010
+exports.default = CoordCache;
4011
 
4012
 
4013
 /***/ }),
4014
-/* 55 */
4015
+/* 59 */
4016
 /***/ (function(module, exports, __webpack_require__) {
4017
 
4018
 Object.defineProperty(exports, "__esModule", { value: true });
4019
-var tslib_1 = __webpack_require__(2);
4020
+var $ = __webpack_require__(3);
4021
 var util_1 = __webpack_require__(4);
4022
-var Mixin_1 = __webpack_require__(14);
4023
-/*
4024
-A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns.
4025
-Prerequisite: the object being mixed into needs to be a *Grid*
4026
-*/
4027
-var DayTableMixin = /** @class */ (function (_super) {
4028
-    tslib_1.__extends(DayTableMixin, _super);
4029
-    function DayTableMixin() {
4030
-        return _super !== null && _super.apply(this, arguments) || this;
4031
+var ListenerMixin_1 = __webpack_require__(7);
4032
+var GlobalEmitter_1 = __webpack_require__(23);
4033
+/* Tracks a drag's mouse movement, firing various handlers
4034
+----------------------------------------------------------------------------------------------------------------------*/
4035
+// TODO: use Emitter
4036
+var DragListener = /** @class */ (function () {
4037
+    function DragListener(options) {
4038
+        this.isInteracting = false;
4039
+        this.isDistanceSurpassed = false;
4040
+        this.isDelayEnded = false;
4041
+        this.isDragging = false;
4042
+        this.isTouch = false;
4043
+        this.isGeneric = false; // initiated by 'dragstart' (jqui)
4044
+        this.shouldCancelTouchScroll = true;
4045
+        this.scrollAlwaysKills = false;
4046
+        this.isAutoScroll = false;
4047
+        // defaults
4048
+        this.scrollSensitivity = 30; // pixels from edge for scrolling to start
4049
+        this.scrollSpeed = 200; // pixels per second, at maximum speed
4050
+        this.scrollIntervalMs = 50; // millisecond wait between scroll increment
4051
+        this.options = options || {};
4052
     }
4053
-    // Populates internal variables used for date calculation and rendering
4054
-    DayTableMixin.prototype.updateDayTable = function () {
4055
-        var t = this;
4056
-        var view = t.view;
4057
-        var calendar = view.calendar;
4058
-        var date = calendar.msToUtcMoment(t.dateProfile.renderUnzonedRange.startMs, true);
4059
-        var end = calendar.msToUtcMoment(t.dateProfile.renderUnzonedRange.endMs, true);
4060
-        var dayIndex = -1;
4061
-        var dayIndices = [];
4062
-        var dayDates = [];
4063
-        var daysPerRow;
4064
-        var firstDay;
4065
-        var rowCnt;
4066
-        while (date.isBefore(end)) {
4067
-            if (view.isHiddenDay(date)) {
4068
-                dayIndices.push(dayIndex + 0.5); // mark that it's between indices
4069
+    // Interaction (high-level)
4070
+    // -----------------------------------------------------------------------------------------------------------------
4071
+    DragListener.prototype.startInteraction = function (ev, extraOptions) {
4072
+        if (extraOptions === void 0) { extraOptions = {}; }
4073
+        if (ev.type === 'mousedown') {
4074
+            if (GlobalEmitter_1.default.get().shouldIgnoreMouse()) {
4075
+                return;
4076
+            }
4077
+            else if (!util_1.isPrimaryMouseButton(ev)) {
4078
+                return;
4079
             }
4080
             else {
4081
-                dayIndex++;
4082
-                dayIndices.push(dayIndex);
4083
-                dayDates.push(date.clone());
4084
+                ev.preventDefault(); // prevents native selection in most browsers
4085
             }
4086
-            date.add(1, 'days');
4087
         }
4088
-        if (this.breakOnWeeks) {
4089
-            // count columns until the day-of-week repeats
4090
-            firstDay = dayDates[0].day();
4091
-            for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) {
4092
-                if (dayDates[daysPerRow].day() === firstDay) {
4093
-                    break;
4094
-                }
4095
+        if (!this.isInteracting) {
4096
+            // process options
4097
+            this.delay = util_1.firstDefined(extraOptions.delay, this.options.delay, 0);
4098
+            this.minDistance = util_1.firstDefined(extraOptions.distance, this.options.distance, 0);
4099
+            this.subjectEl = this.options.subjectEl;
4100
+            util_1.preventSelection($('body'));
4101
+            this.isInteracting = true;
4102
+            this.isTouch = util_1.getEvIsTouch(ev);
4103
+            this.isGeneric = ev.type === 'dragstart';
4104
+            this.isDelayEnded = false;
4105
+            this.isDistanceSurpassed = false;
4106
+            this.originX = util_1.getEvX(ev);
4107
+            this.originY = util_1.getEvY(ev);
4108
+            this.scrollEl = util_1.getScrollParent($(ev.target));
4109
+            this.bindHandlers();
4110
+            this.initAutoScroll();
4111
+            this.handleInteractionStart(ev);
4112
+            this.startDelay(ev);
4113
+            if (!this.minDistance) {
4114
+                this.handleDistanceSurpassed(ev);
4115
             }
4116
-            rowCnt = Math.ceil(dayDates.length / daysPerRow);
4117
-        }
4118
-        else {
4119
-            rowCnt = 1;
4120
-            daysPerRow = dayDates.length;
4121
         }
4122
-        this.dayDates = dayDates;
4123
-        this.dayIndices = dayIndices;
4124
-        this.daysPerRow = daysPerRow;
4125
-        this.rowCnt = rowCnt;
4126
-        this.updateDayTableCols();
4127
-    };
4128
-    // Computes and assigned the colCnt property and updates any options that may be computed from it
4129
-    DayTableMixin.prototype.updateDayTableCols = function () {
4130
-        this.colCnt = this.computeColCnt();
4131
-        this.colHeadFormat =
4132
-            this.opt('columnHeaderFormat') ||
4133
-                this.opt('columnFormat') || // deprecated
4134
-                this.computeColHeadFormat();
4135
-    };
4136
-    // Determines how many columns there should be in the table
4137
-    DayTableMixin.prototype.computeColCnt = function () {
4138
-        return this.daysPerRow;
4139
     };
4140
-    // Computes the ambiguously-timed moment for the given cell
4141
-    DayTableMixin.prototype.getCellDate = function (row, col) {
4142
-        return this.dayDates[this.getCellDayIndex(row, col)].clone();
4143
+    DragListener.prototype.handleInteractionStart = function (ev) {
4144
+        this.trigger('interactionStart', ev);
4145
     };
4146
-    // Computes the ambiguously-timed date range for the given cell
4147
-    DayTableMixin.prototype.getCellRange = function (row, col) {
4148
-        var start = this.getCellDate(row, col);
4149
-        var end = start.clone().add(1, 'days');
4150
-        return { start: start, end: end };
4151
+    DragListener.prototype.endInteraction = function (ev, isCancelled) {
4152
+        if (this.isInteracting) {
4153
+            this.endDrag(ev);
4154
+            if (this.delayTimeoutId) {
4155
+                clearTimeout(this.delayTimeoutId);
4156
+                this.delayTimeoutId = null;
4157
+            }
4158
+            this.destroyAutoScroll();
4159
+            this.unbindHandlers();
4160
+            this.isInteracting = false;
4161
+            this.handleInteractionEnd(ev, isCancelled);
4162
+            util_1.allowSelection($('body'));
4163
+        }
4164
     };
4165
-    // Returns the number of day cells, chronologically, from the first of the grid (0-based)
4166
-    DayTableMixin.prototype.getCellDayIndex = function (row, col) {
4167
-        return row * this.daysPerRow + this.getColDayIndex(col);
4168
+    DragListener.prototype.handleInteractionEnd = function (ev, isCancelled) {
4169
+        this.trigger('interactionEnd', ev, isCancelled || false);
4170
     };
4171
-    // Returns the numner of day cells, chronologically, from the first cell in *any given row*
4172
-    DayTableMixin.prototype.getColDayIndex = function (col) {
4173
-        if (this.isRTL) {
4174
-            return this.colCnt - 1 - col;
4175
+    // Binding To DOM
4176
+    // -----------------------------------------------------------------------------------------------------------------
4177
+    DragListener.prototype.bindHandlers = function () {
4178
+        // some browsers (Safari in iOS 10) don't allow preventDefault on touch events that are bound after touchstart,
4179
+        // so listen to the GlobalEmitter singleton, which is always bound, instead of the document directly.
4180
+        var globalEmitter = GlobalEmitter_1.default.get();
4181
+        if (this.isGeneric) {
4182
+            this.listenTo($(document), {
4183
+                drag: this.handleMove,
4184
+                dragstop: this.endInteraction
4185
+            });
4186
+        }
4187
+        else if (this.isTouch) {
4188
+            this.listenTo(globalEmitter, {
4189
+                touchmove: this.handleTouchMove,
4190
+                touchend: this.endInteraction,
4191
+                scroll: this.handleTouchScroll
4192
+            });
4193
         }
4194
         else {
4195
-            return col;
4196
+            this.listenTo(globalEmitter, {
4197
+                mousemove: this.handleMouseMove,
4198
+                mouseup: this.endInteraction
4199
+            });
4200
         }
4201
+        this.listenTo(globalEmitter, {
4202
+            selectstart: util_1.preventDefault,
4203
+            contextmenu: util_1.preventDefault // long taps would open menu on Chrome dev tools
4204
+        });
4205
     };
4206
-    // Given a date, returns its chronolocial cell-index from the first cell of the grid.
4207
-    // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
4208
-    // If before the first offset, returns a negative number.
4209
-    // If after the last offset, returns an offset past the last cell offset.
4210
-    // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
4211
-    DayTableMixin.prototype.getDateDayIndex = function (date) {
4212
-        var dayIndices = this.dayIndices;
4213
-        var dayOffset = date.diff(this.dayDates[0], 'days');
4214
-        if (dayOffset < 0) {
4215
-            return dayIndices[0] - 1;
4216
+    DragListener.prototype.unbindHandlers = function () {
4217
+        this.stopListeningTo(GlobalEmitter_1.default.get());
4218
+        this.stopListeningTo($(document)); // for isGeneric
4219
+    };
4220
+    // Drag (high-level)
4221
+    // -----------------------------------------------------------------------------------------------------------------
4222
+    // extraOptions ignored if drag already started
4223
+    DragListener.prototype.startDrag = function (ev, extraOptions) {
4224
+        this.startInteraction(ev, extraOptions); // ensure interaction began
4225
+        if (!this.isDragging) {
4226
+            this.isDragging = true;
4227
+            this.handleDragStart(ev);
4228
         }
4229
-        else if (dayOffset >= dayIndices.length) {
4230
-            return dayIndices[dayIndices.length - 1] + 1;
4231
+    };
4232
+    DragListener.prototype.handleDragStart = function (ev) {
4233
+        this.trigger('dragStart', ev);
4234
+    };
4235
+    DragListener.prototype.handleMove = function (ev) {
4236
+        var dx = util_1.getEvX(ev) - this.originX;
4237
+        var dy = util_1.getEvY(ev) - this.originY;
4238
+        var minDistance = this.minDistance;
4239
+        var distanceSq; // current distance from the origin, squared
4240
+        if (!this.isDistanceSurpassed) {
4241
+            distanceSq = dx * dx + dy * dy;
4242
+            if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
4243
+                this.handleDistanceSurpassed(ev);
4244
+            }
4245
         }
4246
-        else {
4247
-            return dayIndices[dayOffset];
4248
+        if (this.isDragging) {
4249
+            this.handleDrag(dx, dy, ev);
4250
         }
4251
     };
4252
-    /* Options
4253
-    ------------------------------------------------------------------------------------------------------------------*/
4254
-    // Computes a default column header formatting string if `colFormat` is not explicitly defined
4255
-    DayTableMixin.prototype.computeColHeadFormat = function () {
4256
-        // if more than one week row, or if there are a lot of columns with not much space,
4257
-        // put just the day numbers will be in each cell
4258
-        if (this.rowCnt > 1 || this.colCnt > 10) {
4259
-            return 'ddd'; // "Sat"
4260
+    // Called while the mouse is being moved and when we know a legitimate drag is taking place
4261
+    DragListener.prototype.handleDrag = function (dx, dy, ev) {
4262
+        this.trigger('drag', dx, dy, ev);
4263
+        this.updateAutoScroll(ev); // will possibly cause scrolling
4264
+    };
4265
+    DragListener.prototype.endDrag = function (ev) {
4266
+        if (this.isDragging) {
4267
+            this.isDragging = false;
4268
+            this.handleDragEnd(ev);
4269
         }
4270
-        else if (this.colCnt > 1) {
4271
-            return this.opt('dayOfMonthFormat'); // "Sat 12/10"
4272
+    };
4273
+    DragListener.prototype.handleDragEnd = function (ev) {
4274
+        this.trigger('dragEnd', ev);
4275
+    };
4276
+    // Delay
4277
+    // -----------------------------------------------------------------------------------------------------------------
4278
+    DragListener.prototype.startDelay = function (initialEv) {
4279
+        var _this = this;
4280
+        if (this.delay) {
4281
+            this.delayTimeoutId = setTimeout(function () {
4282
+                _this.handleDelayEnd(initialEv);
4283
+            }, this.delay);
4284
         }
4285
         else {
4286
-            return 'dddd'; // "Saturday"
4287
+            this.handleDelayEnd(initialEv);
4288
         }
4289
     };
4290
-    /* Slicing
4291
-    ------------------------------------------------------------------------------------------------------------------*/
4292
-    // Slices up a date range into a segment for every week-row it intersects with
4293
-    DayTableMixin.prototype.sliceRangeByRow = function (unzonedRange) {
4294
-        var daysPerRow = this.daysPerRow;
4295
-        var normalRange = this.view.computeDayRange(unzonedRange); // make whole-day range, considering nextDayThreshold
4296
-        var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
4297
-        var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
4298
-        var segs = [];
4299
-        var row;
4300
-        var rowFirst;
4301
-        var rowLast; // inclusive day-index range for current row
4302
-        var segFirst;
4303
-        var segLast; // inclusive day-index range for segment
4304
-        for (row = 0; row < this.rowCnt; row++) {
4305
-            rowFirst = row * daysPerRow;
4306
-            rowLast = rowFirst + daysPerRow - 1;
4307
-            // intersect segment's offset range with the row's
4308
-            segFirst = Math.max(rangeFirst, rowFirst);
4309
-            segLast = Math.min(rangeLast, rowLast);
4310
-            // deal with in-between indices
4311
-            segFirst = Math.ceil(segFirst); // in-between starts round to next cell
4312
-            segLast = Math.floor(segLast); // in-between ends round to prev cell
4313
-            if (segFirst <= segLast) {
4314
-                segs.push({
4315
-                    row: row,
4316
-                    // normalize to start of row
4317
-                    firstRowDayIndex: segFirst - rowFirst,
4318
-                    lastRowDayIndex: segLast - rowFirst,
4319
-                    // must be matching integers to be the segment's start/end
4320
-                    isStart: segFirst === rangeFirst,
4321
-                    isEnd: segLast === rangeLast
4322
-                });
4323
-            }
4324
+    DragListener.prototype.handleDelayEnd = function (initialEv) {
4325
+        this.isDelayEnded = true;
4326
+        if (this.isDistanceSurpassed) {
4327
+            this.startDrag(initialEv);
4328
         }
4329
-        return segs;
4330
     };
4331
-    // Slices up a date range into a segment for every day-cell it intersects with.
4332
-    // TODO: make more DRY with sliceRangeByRow somehow.
4333
-    DayTableMixin.prototype.sliceRangeByDay = function (unzonedRange) {
4334
-        var daysPerRow = this.daysPerRow;
4335
-        var normalRange = this.view.computeDayRange(unzonedRange); // make whole-day range, considering nextDayThreshold
4336
-        var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
4337
-        var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
4338
-        var segs = [];
4339
-        var row;
4340
-        var rowFirst;
4341
-        var rowLast; // inclusive day-index range for current row
4342
-        var i;
4343
-        var segFirst;
4344
-        var segLast; // inclusive day-index range for segment
4345
-        for (row = 0; row < this.rowCnt; row++) {
4346
-            rowFirst = row * daysPerRow;
4347
-            rowLast = rowFirst + daysPerRow - 1;
4348
-            for (i = rowFirst; i <= rowLast; i++) {
4349
-                // intersect segment's offset range with the row's
4350
-                segFirst = Math.max(rangeFirst, i);
4351
-                segLast = Math.min(rangeLast, i);
4352
-                // deal with in-between indices
4353
-                segFirst = Math.ceil(segFirst); // in-between starts round to next cell
4354
-                segLast = Math.floor(segLast); // in-between ends round to prev cell
4355
-                if (segFirst <= segLast) {
4356
-                    segs.push({
4357
-                        row: row,
4358
-                        // normalize to start of row
4359
-                        firstRowDayIndex: segFirst - rowFirst,
4360
-                        lastRowDayIndex: segLast - rowFirst,
4361
-                        // must be matching integers to be the segment's start/end
4362
-                        isStart: segFirst === rangeFirst,
4363
-                        isEnd: segLast === rangeLast
4364
-                    });
4365
-                }
4366
-            }
4367
+    // Distance
4368
+    // -----------------------------------------------------------------------------------------------------------------
4369
+    DragListener.prototype.handleDistanceSurpassed = function (ev) {
4370
+        this.isDistanceSurpassed = true;
4371
+        if (this.isDelayEnded) {
4372
+            this.startDrag(ev);
4373
         }
4374
-        return segs;
4375
-    };
4376
-    /* Header Rendering
4377
-    ------------------------------------------------------------------------------------------------------------------*/
4378
-    DayTableMixin.prototype.renderHeadHtml = function () {
4379
-        var theme = this.view.calendar.theme;
4380
-        return '' +
4381
-            '<div class="fc-row ' + theme.getClass('headerRow') + '">' +
4382
-            '<table class="' + theme.getClass('tableGrid') + '">' +
4383
-            '<thead>' +
4384
-            this.renderHeadTrHtml() +
4385
-            '</thead>' +
4386
-            '</table>' +
4387
-            '</div>';
4388
     };
4389
-    DayTableMixin.prototype.renderHeadIntroHtml = function () {
4390
-        return this.renderIntroHtml(); // fall back to generic
4391
+    // Mouse / Touch
4392
+    // -----------------------------------------------------------------------------------------------------------------
4393
+    DragListener.prototype.handleTouchMove = function (ev) {
4394
+        // prevent inertia and touchmove-scrolling while dragging
4395
+        if (this.isDragging && this.shouldCancelTouchScroll) {
4396
+            ev.preventDefault();
4397
+        }
4398
+        this.handleMove(ev);
4399
     };
4400
-    DayTableMixin.prototype.renderHeadTrHtml = function () {
4401
-        return '' +
4402
-            '<tr>' +
4403
-            (this.isRTL ? '' : this.renderHeadIntroHtml()) +
4404
-            this.renderHeadDateCellsHtml() +
4405
-            (this.isRTL ? this.renderHeadIntroHtml() : '') +
4406
-            '</tr>';
4407
+    DragListener.prototype.handleMouseMove = function (ev) {
4408
+        this.handleMove(ev);
4409
     };
4410
-    DayTableMixin.prototype.renderHeadDateCellsHtml = function () {
4411
-        var htmls = [];
4412
-        var col;
4413
-        var date;
4414
-        for (col = 0; col < this.colCnt; col++) {
4415
-            date = this.getCellDate(0, col);
4416
-            htmls.push(this.renderHeadDateCellHtml(date));
4417
+    // Scrolling (unrelated to auto-scroll)
4418
+    // -----------------------------------------------------------------------------------------------------------------
4419
+    DragListener.prototype.handleTouchScroll = function (ev) {
4420
+        // if the drag is being initiated by touch, but a scroll happens before
4421
+        // the drag-initiating delay is over, cancel the drag
4422
+        if (!this.isDragging || this.scrollAlwaysKills) {
4423
+            this.endInteraction(ev, true); // isCancelled=true
4424
         }
4425
-        return htmls.join('');
4426
     };
4427
-    // TODO: when internalApiVersion, accept an object for HTML attributes
4428
-    // (colspan should be no different)
4429
-    DayTableMixin.prototype.renderHeadDateCellHtml = function (date, colspan, otherAttrs) {
4430
-        var t = this;
4431
-        var view = t.view;
4432
-        var isDateValid = t.dateProfile.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
4433
-        var classNames = [
4434
-            'fc-day-header',
4435
-            view.calendar.theme.getClass('widgetHeader')
4436
-        ];
4437
-        var innerHtml;
4438
-        if (typeof t.opt('columnHeaderHtml') === 'function') {
4439
-            innerHtml = t.opt('columnHeaderHtml')(date);
4440
-        }
4441
-        else if (typeof t.opt('columnHeaderText') === 'function') {
4442
-            innerHtml = util_1.htmlEscape(t.opt('columnHeaderText')(date));
4443
+    // Utils
4444
+    // -----------------------------------------------------------------------------------------------------------------
4445
+    // Triggers a callback. Calls a function in the option hash of the same name.
4446
+    // Arguments beyond the first `name` are forwarded on.
4447
+    DragListener.prototype.trigger = function (name) {
4448
+        var args = [];
4449
+        for (var _i = 1; _i < arguments.length; _i++) {
4450
+            args[_i - 1] = arguments[_i];
4451
         }
4452
-        else {
4453
-            innerHtml = util_1.htmlEscape(date.format(t.colHeadFormat));
4454
+        if (this.options[name]) {
4455
+            this.options[name].apply(this, args);
4456
         }
4457
-        // if only one row of days, the classNames on the header can represent the specific days beneath
4458
-        if (t.rowCnt === 1) {
4459
-            classNames = classNames.concat(
4460
-            // includes the day-of-week class
4461
-            // noThemeHighlight=true (don't highlight the header)
4462
-            t.getDayClasses(date, true));
4463
+        // makes _methods callable by event name. TODO: kill this
4464
+        if (this['_' + name]) {
4465
+            this['_' + name].apply(this, args);
4466
         }
4467
-        else {
4468
-            classNames.push('fc-' + util_1.dayIDs[date.day()]); // only add the day-of-week class
4469
+    };
4470
+    // Auto-scroll
4471
+    // -----------------------------------------------------------------------------------------------------------------
4472
+    DragListener.prototype.initAutoScroll = function () {
4473
+        var scrollEl = this.scrollEl;
4474
+        this.isAutoScroll =
4475
+            this.options.scroll &&
4476
+                scrollEl &&
4477
+                !scrollEl.is(window) &&
4478
+                !scrollEl.is(document);
4479
+        if (this.isAutoScroll) {
4480
+            // debounce makes sure rapid calls don't happen
4481
+            this.listenTo(scrollEl, 'scroll', util_1.debounce(this.handleDebouncedScroll, 100));
4482
         }
4483
-        return '' +
4484
-            '<th class="' + classNames.join(' ') + '"' +
4485
-            ((isDateValid && t.rowCnt) === 1 ?
4486
-                ' data-date="' + date.format('YYYY-MM-DD') + '"' :
4487
-                '') +
4488
-            (colspan > 1 ?
4489
-                ' colspan="' + colspan + '"' :
4490
-                '') +
4491
-            (otherAttrs ?
4492
-                ' ' + otherAttrs :
4493
-                '') +
4494
-            '>' +
4495
-            (isDateValid ?
4496
-                // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff)
4497
-                view.buildGotoAnchorHtml({ date: date, forceOff: t.rowCnt > 1 || t.colCnt === 1 }, innerHtml) :
4498
-                // if not valid, display text, but no link
4499
-                innerHtml) +
4500
-            '</th>';
4501
     };
4502
-    /* Background Rendering
4503
-    ------------------------------------------------------------------------------------------------------------------*/
4504
-    DayTableMixin.prototype.renderBgTrHtml = function (row) {
4505
-        return '' +
4506
-            '<tr>' +
4507
-            (this.isRTL ? '' : this.renderBgIntroHtml(row)) +
4508
-            this.renderBgCellsHtml(row) +
4509
-            (this.isRTL ? this.renderBgIntroHtml(row) : '') +
4510
-            '</tr>';
4511
+    DragListener.prototype.destroyAutoScroll = function () {
4512
+        this.endAutoScroll(); // kill any animation loop
4513
+        // remove the scroll handler if there is a scrollEl
4514
+        if (this.isAutoScroll) {
4515
+            this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :(
4516
+        }
4517
     };
4518
-    DayTableMixin.prototype.renderBgIntroHtml = function (row) {
4519
-        return this.renderIntroHtml(); // fall back to generic
4520
+    // Computes and stores the bounding rectangle of scrollEl
4521
+    DragListener.prototype.computeScrollBounds = function () {
4522
+        if (this.isAutoScroll) {
4523
+            this.scrollBounds = util_1.getOuterRect(this.scrollEl);
4524
+            // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
4525
+        }
4526
     };
4527
-    DayTableMixin.prototype.renderBgCellsHtml = function (row) {
4528
-        var htmls = [];
4529
-        var col;
4530
-        var date;
4531
-        for (col = 0; col < this.colCnt; col++) {
4532
-            date = this.getCellDate(row, col);
4533
-            htmls.push(this.renderBgCellHtml(date));
4534
+    // Called when the dragging is in progress and scrolling should be updated
4535
+    DragListener.prototype.updateAutoScroll = function (ev) {
4536
+        var sensitivity = this.scrollSensitivity;
4537
+        var bounds = this.scrollBounds;
4538
+        var topCloseness;
4539
+        var bottomCloseness;
4540
+        var leftCloseness;
4541
+        var rightCloseness;
4542
+        var topVel = 0;
4543
+        var leftVel = 0;
4544
+        if (bounds) { // only scroll if scrollEl exists
4545
+            // compute closeness to edges. valid range is from 0.0 - 1.0
4546
+            topCloseness = (sensitivity - (util_1.getEvY(ev) - bounds.top)) / sensitivity;
4547
+            bottomCloseness = (sensitivity - (bounds.bottom - util_1.getEvY(ev))) / sensitivity;
4548
+            leftCloseness = (sensitivity - (util_1.getEvX(ev) - bounds.left)) / sensitivity;
4549
+            rightCloseness = (sensitivity - (bounds.right - util_1.getEvX(ev))) / sensitivity;
4550
+            // translate vertical closeness into velocity.
4551
+            // mouse must be completely in bounds for velocity to happen.
4552
+            if (topCloseness >= 0 && topCloseness <= 1) {
4553
+                topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
4554
+            }
4555
+            else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
4556
+                topVel = bottomCloseness * this.scrollSpeed;
4557
+            }
4558
+            // translate horizontal closeness into velocity
4559
+            if (leftCloseness >= 0 && leftCloseness <= 1) {
4560
+                leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
4561
+            }
4562
+            else if (rightCloseness >= 0 && rightCloseness <= 1) {
4563
+                leftVel = rightCloseness * this.scrollSpeed;
4564
+            }
4565
         }
4566
-        return htmls.join('');
4567
+        this.setScrollVel(topVel, leftVel);
4568
     };
4569
-    DayTableMixin.prototype.renderBgCellHtml = function (date, otherAttrs) {
4570
-        var t = this;
4571
-        var view = t.view;
4572
-        var isDateValid = t.dateProfile.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
4573
-        var classes = t.getDayClasses(date);
4574
-        classes.unshift('fc-day', view.calendar.theme.getClass('widgetContent'));
4575
-        return '<td class="' + classes.join(' ') + '"' +
4576
-            (isDateValid ?
4577
-                ' data-date="' + date.format('YYYY-MM-DD') + '"' : // if date has a time, won't format it
4578
-                '') +
4579
-            (otherAttrs ?
4580
-                ' ' + otherAttrs :
4581
-                '') +
4582
-            '></td>';
4583
-    };
4584
-    /* Generic
4585
-    ------------------------------------------------------------------------------------------------------------------*/
4586
-    DayTableMixin.prototype.renderIntroHtml = function () {
4587
-        // Generates the default HTML intro for any row. User classes should override
4588
+    // Sets the speed-of-scrolling for the scrollEl
4589
+    DragListener.prototype.setScrollVel = function (topVel, leftVel) {
4590
+        this.scrollTopVel = topVel;
4591
+        this.scrollLeftVel = leftVel;
4592
+        this.constrainScrollVel(); // massages into realistic values
4593
+        // if there is non-zero velocity, and an animation loop hasn't already started, then START
4594
+        if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
4595
+            this.scrollIntervalId = setInterval(util_1.proxy(this, 'scrollIntervalFunc'), // scope to `this`
4596
+            this.scrollIntervalMs);
4597
+        }
4598
     };
4599
-    // TODO: a generic method for dealing with <tr>, RTL, intro
4600
-    // when increment internalApiVersion
4601
-    // wrapTr (scheduler)
4602
-    /* Utils
4603
-    ------------------------------------------------------------------------------------------------------------------*/
4604
-    // Applies the generic "intro" and "outro" HTML to the given cells.
4605
-    // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
4606
-    DayTableMixin.prototype.bookendCells = function (trEl) {
4607
-        var introHtml = this.renderIntroHtml();
4608
-        if (introHtml) {
4609
-            if (this.isRTL) {
4610
-                trEl.append(introHtml);
4611
+    // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
4612
+    DragListener.prototype.constrainScrollVel = function () {
4613
+        var el = this.scrollEl;
4614
+        if (this.scrollTopVel < 0) { // scrolling up?
4615
+            if (el.scrollTop() <= 0) { // already scrolled all the way up?
4616
+                this.scrollTopVel = 0;
4617
             }
4618
-            else {
4619
-                trEl.prepend(introHtml);
4620
+        }
4621
+        else if (this.scrollTopVel > 0) { // scrolling down?
4622
+            if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?
4623
+                this.scrollTopVel = 0;
4624
+            }
4625
+        }
4626
+        if (this.scrollLeftVel < 0) { // scrolling left?
4627
+            if (el.scrollLeft() <= 0) { // already scrolled all the left?
4628
+                this.scrollLeftVel = 0;
4629
+            }
4630
+        }
4631
+        else if (this.scrollLeftVel > 0) { // scrolling right?
4632
+            if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?
4633
+                this.scrollLeftVel = 0;
4634
             }
4635
         }
4636
     };
4637
-    return DayTableMixin;
4638
-}(Mixin_1.default));
4639
-exports.default = DayTableMixin;
4640
-
4641
-
4642
-/***/ }),
4643
-/* 56 */
4644
-/***/ (function(module, exports) {
4645
-
4646
-Object.defineProperty(exports, "__esModule", { value: true });
4647
-var BusinessHourRenderer = /** @class */ (function () {
4648
-    /*
4649
-    component implements:
4650
-      - eventRangesToEventFootprints
4651
-      - eventFootprintsToSegs
4652
-    */
4653
-    function BusinessHourRenderer(component, fillRenderer) {
4654
-        this.component = component;
4655
-        this.fillRenderer = fillRenderer;
4656
-    }
4657
-    BusinessHourRenderer.prototype.render = function (businessHourGenerator) {
4658
-        var component = this.component;
4659
-        var unzonedRange = component._getDateProfile().activeUnzonedRange;
4660
-        var eventInstanceGroup = businessHourGenerator.buildEventInstanceGroup(component.hasAllDayBusinessHours, unzonedRange);
4661
-        var eventFootprints = eventInstanceGroup ?
4662
-            component.eventRangesToEventFootprints(eventInstanceGroup.sliceRenderRanges(unzonedRange)) :
4663
-            [];
4664
-        this.renderEventFootprints(eventFootprints);
4665
-    };
4666
-    BusinessHourRenderer.prototype.renderEventFootprints = function (eventFootprints) {
4667
-        var segs = this.component.eventFootprintsToSegs(eventFootprints);
4668
-        this.renderSegs(segs);
4669
-        this.segs = segs;
4670
+    // This function gets called during every iteration of the scrolling animation loop
4671
+    DragListener.prototype.scrollIntervalFunc = function () {
4672
+        var el = this.scrollEl;
4673
+        var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
4674
+        // change the value of scrollEl's scroll
4675
+        if (this.scrollTopVel) {
4676
+            el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
4677
+        }
4678
+        if (this.scrollLeftVel) {
4679
+            el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
4680
+        }
4681
+        this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
4682
+        // if scrolled all the way, which causes the vels to be zero, stop the animation loop
4683
+        if (!this.scrollTopVel && !this.scrollLeftVel) {
4684
+            this.endAutoScroll();
4685
+        }
4686
     };
4687
-    BusinessHourRenderer.prototype.renderSegs = function (segs) {
4688
-        if (this.fillRenderer) {
4689
-            this.fillRenderer.renderSegs('businessHours', segs, {
4690
-                getClasses: function (seg) {
4691
-                    return ['fc-nonbusiness', 'fc-bgevent'];
4692
-                }
4693
-            });
4694
+    // Kills any existing scrolling animation loop
4695
+    DragListener.prototype.endAutoScroll = function () {
4696
+        if (this.scrollIntervalId) {
4697
+            clearInterval(this.scrollIntervalId);
4698
+            this.scrollIntervalId = null;
4699
+            this.handleScrollEnd();
4700
         }
4701
     };
4702
-    BusinessHourRenderer.prototype.unrender = function () {
4703
-        if (this.fillRenderer) {
4704
-            this.fillRenderer.unrender('businessHours');
4705
+    // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
4706
+    DragListener.prototype.handleDebouncedScroll = function () {
4707
+        // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
4708
+        if (!this.scrollIntervalId) {
4709
+            this.handleScrollEnd();
4710
         }
4711
-        this.segs = null;
4712
     };
4713
-    BusinessHourRenderer.prototype.getSegs = function () {
4714
-        return this.segs || [];
4715
+    DragListener.prototype.handleScrollEnd = function () {
4716
+        // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
4717
     };
4718
-    return BusinessHourRenderer;
4719
+    return DragListener;
4720
 }());
4721
-exports.default = BusinessHourRenderer;
4722
+exports.default = DragListener;
4723
+ListenerMixin_1.default.mixInto(DragListener);
4724
 
4725
 
4726
 /***/ }),
4727
-/* 57 */
4728
+/* 60 */
4729
 /***/ (function(module, exports, __webpack_require__) {
4730
 
4731
 Object.defineProperty(exports, "__esModule", { value: true });
4732
-var $ = __webpack_require__(3);
4733
+var tslib_1 = __webpack_require__(2);
4734
 var util_1 = __webpack_require__(4);
4735
-var FillRenderer = /** @class */ (function () {
4736
-    function FillRenderer(component) {
4737
-        this.fillSegTag = 'div';
4738
-        this.component = component;
4739
-        this.elsByFill = {};
4740
+var Mixin_1 = __webpack_require__(15);
4741
+/*
4742
+A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns.
4743
+Prerequisite: the object being mixed into needs to be a *Grid*
4744
+*/
4745
+var DayTableMixin = /** @class */ (function (_super) {
4746
+    tslib_1.__extends(DayTableMixin, _super);
4747
+    function DayTableMixin() {
4748
+        return _super !== null && _super.apply(this, arguments) || this;
4749
     }
4750
-    FillRenderer.prototype.renderFootprint = function (type, componentFootprint, props) {
4751
-        this.renderSegs(type, this.component.componentFootprintToSegs(componentFootprint), props);
4752
-    };
4753
-    FillRenderer.prototype.renderSegs = function (type, segs, props) {
4754
-        var els;
4755
-        segs = this.buildSegEls(type, segs, props); // assignes `.el` to each seg. returns successfully rendered segs
4756
-        els = this.attachSegEls(type, segs);
4757
-        if (els) {
4758
-            this.reportEls(type, els);
4759
-        }
4760
-        return segs;
4761
-    };
4762
-    // Unrenders a specific type of fill that is currently rendered on the grid
4763
-    FillRenderer.prototype.unrender = function (type) {
4764
-        var el = this.elsByFill[type];
4765
-        if (el) {
4766
-            el.remove();
4767
-            delete this.elsByFill[type];
4768
-        }
4769
-    };
4770
-    // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
4771
-    // Only returns segments that successfully rendered.
4772
-    FillRenderer.prototype.buildSegEls = function (type, segs, props) {
4773
-        var _this = this;
4774
-        var html = '';
4775
-        var renderedSegs = [];
4776
-        var i;
4777
-        if (segs.length) {
4778
-            // build a large concatenation of segment HTML
4779
-            for (i = 0; i < segs.length; i++) {
4780
-                html += this.buildSegHtml(type, segs[i], props);
4781
+    // Populates internal variables used for date calculation and rendering
4782
+    DayTableMixin.prototype.updateDayTable = function () {
4783
+        var t = this;
4784
+        var view = t.view;
4785
+        var calendar = view.calendar;
4786
+        var date = calendar.msToUtcMoment(t.dateProfile.renderUnzonedRange.startMs, true);
4787
+        var end = calendar.msToUtcMoment(t.dateProfile.renderUnzonedRange.endMs, true);
4788
+        var dayIndex = -1;
4789
+        var dayIndices = [];
4790
+        var dayDates = [];
4791
+        var daysPerRow;
4792
+        var firstDay;
4793
+        var rowCnt;
4794
+        while (date.isBefore(end)) { // loop each day from start to end
4795
+            if (view.isHiddenDay(date)) {
4796
+                dayIndices.push(dayIndex + 0.5); // mark that it's between indices
4797
             }
4798
-            // Grab individual elements from the combined HTML string. Use each as the default rendering.
4799
-            // Then, compute the 'el' for each segment.
4800
-            $(html).each(function (i, node) {
4801
-                var seg = segs[i];
4802
-                var el = $(node);
4803
-                // allow custom filter methods per-type
4804
-                if (props.filterEl) {
4805
-                    el = props.filterEl(seg, el);
4806
-                }
4807
-                if (el) {
4808
-                    el = $(el); // allow custom filter to return raw DOM node
4809
-                    // correct element type? (would be bad if a non-TD were inserted into a table for example)
4810
-                    if (el.is(_this.fillSegTag)) {
4811
-                        seg.el = el;
4812
-                        renderedSegs.push(seg);
4813
-                    }
4814
-                }
4815
-            });
4816
+            else {
4817
+                dayIndex++;
4818
+                dayIndices.push(dayIndex);
4819
+                dayDates.push(date.clone());
4820
+            }
4821
+            date.add(1, 'days');
4822
         }
4823
-        return renderedSegs;
4824
-    };
4825
-    // Builds the HTML needed for one fill segment. Generic enough to work with different types.
4826
-    FillRenderer.prototype.buildSegHtml = function (type, seg, props) {
4827
-        // custom hooks per-type
4828
-        var classes = props.getClasses ? props.getClasses(seg) : [];
4829
-        var css = util_1.cssToStr(props.getCss ? props.getCss(seg) : {});
4830
-        return '<' + this.fillSegTag +
4831
-            (classes.length ? ' class="' + classes.join(' ') + '"' : '') +
4832
-            (css ? ' style="' + css + '"' : '') +
4833
-            ' />';
4834
-    };
4835
-    // Should return wrapping DOM structure
4836
-    FillRenderer.prototype.attachSegEls = function (type, segs) {
4837
-        // subclasses must implement
4838
-    };
4839
-    FillRenderer.prototype.reportEls = function (type, nodes) {
4840
-        if (this.elsByFill[type]) {
4841
-            this.elsByFill[type] = this.elsByFill[type].add(nodes);
4842
+        if (this.breakOnWeeks) {
4843
+            // count columns until the day-of-week repeats
4844
+            firstDay = dayDates[0].day();
4845
+            for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) {
4846
+                if (dayDates[daysPerRow].day() === firstDay) {
4847
+                    break;
4848
+                }
4849
+            }
4850
+            rowCnt = Math.ceil(dayDates.length / daysPerRow);
4851
         }
4852
         else {
4853
-            this.elsByFill[type] = $(nodes);
4854
+            rowCnt = 1;
4855
+            daysPerRow = dayDates.length;
4856
         }
4857
+        this.dayDates = dayDates;
4858
+        this.dayIndices = dayIndices;
4859
+        this.daysPerRow = daysPerRow;
4860
+        this.rowCnt = rowCnt;
4861
+        this.updateDayTableCols();
4862
     };
4863
-    return FillRenderer;
4864
-}());
4865
-exports.default = FillRenderer;
4866
-
4867
-
4868
-/***/ }),
4869
-/* 58 */
4870
-/***/ (function(module, exports, __webpack_require__) {
4871
-
4872
-Object.defineProperty(exports, "__esModule", { value: true });
4873
-var SingleEventDef_1 = __webpack_require__(13);
4874
-var EventFootprint_1 = __webpack_require__(36);
4875
-var EventSource_1 = __webpack_require__(6);
4876
-var HelperRenderer = /** @class */ (function () {
4877
-    function HelperRenderer(component, eventRenderer) {
4878
-        this.view = component._getView();
4879
-        this.component = component;
4880
-        this.eventRenderer = eventRenderer;
4881
-    }
4882
-    HelperRenderer.prototype.renderComponentFootprint = function (componentFootprint) {
4883
-        this.renderEventFootprints([
4884
-            this.fabricateEventFootprint(componentFootprint)
4885
-        ]);
4886
-    };
4887
-    HelperRenderer.prototype.renderEventDraggingFootprints = function (eventFootprints, sourceSeg, isTouch) {
4888
-        this.renderEventFootprints(eventFootprints, sourceSeg, 'fc-dragging', isTouch ? null : this.view.opt('dragOpacity'));
4889
-    };
4890
-    HelperRenderer.prototype.renderEventResizingFootprints = function (eventFootprints, sourceSeg, isTouch) {
4891
-        this.renderEventFootprints(eventFootprints, sourceSeg, 'fc-resizing');
4892
+    // Computes and assigned the colCnt property and updates any options that may be computed from it
4893
+    DayTableMixin.prototype.updateDayTableCols = function () {
4894
+        this.colCnt = this.computeColCnt();
4895
+        this.colHeadFormat =
4896
+            this.opt('columnHeaderFormat') ||
4897
+                this.opt('columnFormat') || // deprecated
4898
+                this.computeColHeadFormat();
4899
     };
4900
-    HelperRenderer.prototype.renderEventFootprints = function (eventFootprints, sourceSeg, extraClassNames, opacity) {
4901
-        var segs = this.component.eventFootprintsToSegs(eventFootprints);
4902
-        var classNames = 'fc-helper ' + (extraClassNames || '');
4903
-        var i;
4904
-        // assigns each seg's el and returns a subset of segs that were rendered
4905
-        segs = this.eventRenderer.renderFgSegEls(segs);
4906
-        for (i = 0; i < segs.length; i++) {
4907
-            segs[i].el.addClass(classNames);
4908
-        }
4909
-        if (opacity != null) {
4910
-            for (i = 0; i < segs.length; i++) {
4911
-                segs[i].el.css('opacity', opacity);
4912
-            }
4913
-        }
4914
-        this.helperEls = this.renderSegs(segs, sourceSeg);
4915
+    // Determines how many columns there should be in the table
4916
+    DayTableMixin.prototype.computeColCnt = function () {
4917
+        return this.daysPerRow;
4918
     };
4919
-    /*
4920
-    Must return all mock event elements
4921
-    */
4922
-    HelperRenderer.prototype.renderSegs = function (segs, sourceSeg) {
4923
-        // Subclasses must implement
4924
+    // Computes the ambiguously-timed moment for the given cell
4925
+    DayTableMixin.prototype.getCellDate = function (row, col) {
4926
+        return this.dayDates[this.getCellDayIndex(row, col)].clone();
4927
     };
4928
-    HelperRenderer.prototype.unrender = function () {
4929
-        if (this.helperEls) {
4930
-            this.helperEls.remove();
4931
-            this.helperEls = null;
4932
-        }
4933
+    // Computes the ambiguously-timed date range for the given cell
4934
+    DayTableMixin.prototype.getCellRange = function (row, col) {
4935
+        var start = this.getCellDate(row, col);
4936
+        var end = start.clone().add(1, 'days');
4937
+        return { start: start, end: end };
4938
     };
4939
-    HelperRenderer.prototype.fabricateEventFootprint = function (componentFootprint) {
4940
-        var calendar = this.view.calendar;
4941
-        var eventDateProfile = calendar.footprintToDateProfile(componentFootprint);
4942
-        var dummyEvent = new SingleEventDef_1.default(new EventSource_1.default(calendar));
4943
-        var dummyInstance;
4944
-        dummyEvent.dateProfile = eventDateProfile;
4945
-        dummyInstance = dummyEvent.buildInstance();
4946
-        return new EventFootprint_1.default(componentFootprint, dummyEvent, dummyInstance);
4947
+    // Returns the number of day cells, chronologically, from the first of the grid (0-based)
4948
+    DayTableMixin.prototype.getCellDayIndex = function (row, col) {
4949
+        return row * this.daysPerRow + this.getColDayIndex(col);
4950
     };
4951
-    return HelperRenderer;
4952
-}());
4953
-exports.default = HelperRenderer;
4954
-
4955
-
4956
-/***/ }),
4957
-/* 59 */
4958
-/***/ (function(module, exports, __webpack_require__) {
4959
-
4960
-Object.defineProperty(exports, "__esModule", { value: true });
4961
-var tslib_1 = __webpack_require__(2);
4962
-var GlobalEmitter_1 = __webpack_require__(21);
4963
-var Interaction_1 = __webpack_require__(15);
4964
-var EventPointing = /** @class */ (function (_super) {
4965
-    tslib_1.__extends(EventPointing, _super);
4966
-    function EventPointing() {
4967
-        return _super !== null && _super.apply(this, arguments) || this;
4968
-    }
4969
-    /*
4970
-    component must implement:
4971
-      - publiclyTrigger
4972
-    */
4973
-    EventPointing.prototype.bindToEl = function (el) {
4974
-        var component = this.component;
4975
-        component.bindSegHandlerToEl(el, 'click', this.handleClick.bind(this));
4976
-        component.bindSegHandlerToEl(el, 'mouseenter', this.handleMouseover.bind(this));
4977
-        component.bindSegHandlerToEl(el, 'mouseleave', this.handleMouseout.bind(this));
4978
+    // Returns the numner of day cells, chronologically, from the first cell in *any given row*
4979
+    DayTableMixin.prototype.getColDayIndex = function (col) {
4980
+        if (this.isRTL) {
4981
+            return this.colCnt - 1 - col;
4982
+        }
4983
+        else {
4984
+            return col;
4985
+        }
4986
     };
4987
-    EventPointing.prototype.handleClick = function (seg, ev) {
4988
-        var res = this.component.publiclyTrigger('eventClick', {
4989
-            context: seg.el[0],
4990
-            args: [seg.footprint.getEventLegacy(), ev, this.view]
4991
-        });
4992
-        if (res === false) {
4993
-            ev.preventDefault();
4994
+    // Given a date, returns its chronolocial cell-index from the first cell of the grid.
4995
+    // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
4996
+    // If before the first offset, returns a negative number.
4997
+    // If after the last offset, returns an offset past the last cell offset.
4998
+    // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
4999
+    DayTableMixin.prototype.getDateDayIndex = function (date) {
5000
+        var dayIndices = this.dayIndices;
5001
+        var dayOffset = date.diff(this.dayDates[0], 'days');
5002
+        if (dayOffset < 0) {
5003
+            return dayIndices[0] - 1;
5004
+        }
5005
+        else if (dayOffset >= dayIndices.length) {
5006
+            return dayIndices[dayIndices.length - 1] + 1;
5007
+        }
5008
+        else {
5009
+            return dayIndices[dayOffset];
5010
         }
5011
     };
5012
-    // Updates internal state and triggers handlers for when an event element is moused over
5013
-    EventPointing.prototype.handleMouseover = function (seg, ev) {
5014
-        if (!GlobalEmitter_1.default.get().shouldIgnoreMouse() &&
5015
-            !this.mousedOverSeg) {
5016
-            this.mousedOverSeg = seg;
5017
-            // TODO: move to EventSelecting's responsibility
5018
-            if (this.view.isEventDefResizable(seg.footprint.eventDef)) {
5019
-                seg.el.addClass('fc-allow-mouse-resize');
5020
-            }
5021
-            this.component.publiclyTrigger('eventMouseover', {
5022
-                context: seg.el[0],
5023
-                args: [seg.footprint.getEventLegacy(), ev, this.view]
5024
-            });
5025
+    /* Options
5026
+    ------------------------------------------------------------------------------------------------------------------*/
5027
+    // Computes a default column header formatting string if `colFormat` is not explicitly defined
5028
+    DayTableMixin.prototype.computeColHeadFormat = function () {
5029
+        // if more than one week row, or if there are a lot of columns with not much space,
5030
+        // put just the day numbers will be in each cell
5031
+        if (this.rowCnt > 1 || this.colCnt > 10) {
5032
+            return 'ddd'; // "Sat"
5033
+        }
5034
+        else if (this.colCnt > 1) {
5035
+            return this.opt('dayOfMonthFormat'); // "Sat 12/10"
5036
+        }
5037
+        else {
5038
+            return 'dddd'; // "Saturday"
5039
         }
5040
     };
5041
-    // Updates internal state and triggers handlers for when an event element is moused out.
5042
-    // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
5043
-    EventPointing.prototype.handleMouseout = function (seg, ev) {
5044
-        if (this.mousedOverSeg) {
5045
-            this.mousedOverSeg = null;
5046
-            // TODO: move to EventSelecting's responsibility
5047
-            if (this.view.isEventDefResizable(seg.footprint.eventDef)) {
5048
-                seg.el.removeClass('fc-allow-mouse-resize');
5049
+    /* Slicing
5050
+    ------------------------------------------------------------------------------------------------------------------*/
5051
+    // Slices up a date range into a segment for every week-row it intersects with
5052
+    DayTableMixin.prototype.sliceRangeByRow = function (unzonedRange) {
5053
+        var daysPerRow = this.daysPerRow;
5054
+        var normalRange = this.view.computeDayRange(unzonedRange); // make whole-day range, considering nextDayThreshold
5055
+        var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
5056
+        var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
5057
+        var segs = [];
5058
+        var row;
5059
+        var rowFirst;
5060
+        var rowLast; // inclusive day-index range for current row
5061
+        var segFirst;
5062
+        var segLast; // inclusive day-index range for segment
5063
+        for (row = 0; row < this.rowCnt; row++) {
5064
+            rowFirst = row * daysPerRow;
5065
+            rowLast = rowFirst + daysPerRow - 1;
5066
+            // intersect segment's offset range with the row's
5067
+            segFirst = Math.max(rangeFirst, rowFirst);
5068
+            segLast = Math.min(rangeLast, rowLast);
5069
+            // deal with in-between indices
5070
+            segFirst = Math.ceil(segFirst); // in-between starts round to next cell
5071
+            segLast = Math.floor(segLast); // in-between ends round to prev cell
5072
+            if (segFirst <= segLast) { // was there any intersection with the current row?
5073
+                segs.push({
5074
+                    row: row,
5075
+                    // normalize to start of row
5076
+                    firstRowDayIndex: segFirst - rowFirst,
5077
+                    lastRowDayIndex: segLast - rowFirst,
5078
+                    // must be matching integers to be the segment's start/end
5079
+                    isStart: segFirst === rangeFirst,
5080
+                    isEnd: segLast === rangeLast
5081
+                });
5082
             }
5083
-            this.component.publiclyTrigger('eventMouseout', {
5084
-                context: seg.el[0],
5085
-                args: [
5086
-                    seg.footprint.getEventLegacy(),
5087
-                    ev || {},
5088
-                    this.view
5089
-                ]
5090
-            });
5091
         }
5092
+        return segs;
5093
     };
5094
-    EventPointing.prototype.end = function () {
5095
-        if (this.mousedOverSeg) {
5096
-            this.handleMouseout(this.mousedOverSeg);
5097
+    // Slices up a date range into a segment for every day-cell it intersects with.
5098
+    // TODO: make more DRY with sliceRangeByRow somehow.
5099
+    DayTableMixin.prototype.sliceRangeByDay = function (unzonedRange) {
5100
+        var daysPerRow = this.daysPerRow;
5101
+        var normalRange = this.view.computeDayRange(unzonedRange); // make whole-day range, considering nextDayThreshold
5102
+        var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
5103
+        var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
5104
+        var segs = [];
5105
+        var row;
5106
+        var rowFirst;
5107
+        var rowLast; // inclusive day-index range for current row
5108
+        var i;
5109
+        var segFirst;
5110
+        var segLast; // inclusive day-index range for segment
5111
+        for (row = 0; row < this.rowCnt; row++) {
5112
+            rowFirst = row * daysPerRow;
5113
+            rowLast = rowFirst + daysPerRow - 1;
5114
+            for (i = rowFirst; i <= rowLast; i++) {
5115
+                // intersect segment's offset range with the row's
5116
+                segFirst = Math.max(rangeFirst, i);
5117
+                segLast = Math.min(rangeLast, i);
5118
+                // deal with in-between indices
5119
+                segFirst = Math.ceil(segFirst); // in-between starts round to next cell
5120
+                segLast = Math.floor(segLast); // in-between ends round to prev cell
5121
+                if (segFirst <= segLast) { // was there any intersection with the current row?
5122
+                    segs.push({
5123
+                        row: row,
5124
+                        // normalize to start of row
5125
+                        firstRowDayIndex: segFirst - rowFirst,
5126
+                        lastRowDayIndex: segLast - rowFirst,
5127
+                        // must be matching integers to be the segment's start/end
5128
+                        isStart: segFirst === rangeFirst,
5129
+                        isEnd: segLast === rangeLast
5130
+                    });
5131
+                }
5132
+            }
5133
         }
5134
+        return segs;
5135
     };
5136
-    return EventPointing;
5137
-}(Interaction_1.default));
5138
-exports.default = EventPointing;
5139
-
5140
-
5141
-/***/ }),
5142
-/* 60 */
5143
-/***/ (function(module, exports, __webpack_require__) {
5144
-
5145
-Object.defineProperty(exports, "__esModule", { value: true });
5146
-var tslib_1 = __webpack_require__(2);
5147
-var Mixin_1 = __webpack_require__(14);
5148
-var DateClicking_1 = __webpack_require__(245);
5149
-var DateSelecting_1 = __webpack_require__(225);
5150
-var EventPointing_1 = __webpack_require__(59);
5151
-var EventDragging_1 = __webpack_require__(224);
5152
-var EventResizing_1 = __webpack_require__(223);
5153
-var ExternalDropping_1 = __webpack_require__(222);
5154
-var StandardInteractionsMixin = /** @class */ (function (_super) {
5155
-    tslib_1.__extends(StandardInteractionsMixin, _super);
5156
-    function StandardInteractionsMixin() {
5157
-        return _super !== null && _super.apply(this, arguments) || this;
5158
-    }
5159
-    return StandardInteractionsMixin;
5160
-}(Mixin_1.default));
5161
-exports.default = StandardInteractionsMixin;
5162
-StandardInteractionsMixin.prototype.dateClickingClass = DateClicking_1.default;
5163
-StandardInteractionsMixin.prototype.dateSelectingClass = DateSelecting_1.default;
5164
-StandardInteractionsMixin.prototype.eventPointingClass = EventPointing_1.default;
5165
-StandardInteractionsMixin.prototype.eventDraggingClass = EventDragging_1.default;
5166
-StandardInteractionsMixin.prototype.eventResizingClass = EventResizing_1.default;
5167
-StandardInteractionsMixin.prototype.externalDroppingClass = ExternalDropping_1.default;
5168
-
5169
-
5170
-/***/ }),
5171
-/* 61 */
5172
-/***/ (function(module, exports, __webpack_require__) {
5173
-
5174
-Object.defineProperty(exports, "__esModule", { value: true });
5175
-var tslib_1 = __webpack_require__(2);
5176
-var $ = __webpack_require__(3);
5177
-var util_1 = __webpack_require__(4);
5178
-var CoordCache_1 = __webpack_require__(53);
5179
-var Popover_1 = __webpack_require__(249);
5180
-var UnzonedRange_1 = __webpack_require__(5);
5181
-var ComponentFootprint_1 = __webpack_require__(12);
5182
-var EventFootprint_1 = __webpack_require__(36);
5183
-var BusinessHourRenderer_1 = __webpack_require__(56);
5184
-var StandardInteractionsMixin_1 = __webpack_require__(60);
5185
-var InteractiveDateComponent_1 = __webpack_require__(40);
5186
-var DayTableMixin_1 = __webpack_require__(55);
5187
-var DayGridEventRenderer_1 = __webpack_require__(250);
5188
-var DayGridHelperRenderer_1 = __webpack_require__(251);
5189
-var DayGridFillRenderer_1 = __webpack_require__(252);
5190
-/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
5191
-----------------------------------------------------------------------------------------------------------------------*/
5192
-var DayGrid = /** @class */ (function (_super) {
5193
-    tslib_1.__extends(DayGrid, _super);
5194
-    function DayGrid(view) {
5195
-        var _this = _super.call(this, view) || this;
5196
-        _this.cellWeekNumbersVisible = false; // display week numbers in day cell?
5197
-        _this.bottomCoordPadding = 0; // hack for extending the hit area for the last row of the coordinate grid
5198
-        // isRigid determines whether the individual rows should ignore the contents and be a constant height.
5199
-        // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
5200
-        _this.isRigid = false;
5201
-        _this.hasAllDayBusinessHours = true;
5202
-        return _this;
5203
-    }
5204
-    // Slices up the given span (unzoned start/end with other misc data) into an array of segments
5205
-    DayGrid.prototype.componentFootprintToSegs = function (componentFootprint) {
5206
-        var segs = this.sliceRangeByRow(componentFootprint.unzonedRange);
5207
-        var i;
5208
-        var seg;
5209
-        for (i = 0; i < segs.length; i++) {
5210
-            seg = segs[i];
5211
-            if (this.isRTL) {
5212
-                seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex;
5213
-                seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex;
5214
-            }
5215
-            else {
5216
-                seg.leftCol = seg.firstRowDayIndex;
5217
-                seg.rightCol = seg.lastRowDayIndex;
5218
-            }
5219
-        }
5220
-        return segs;
5221
-    };
5222
-    /* Date Rendering
5223
+    /* Header Rendering
5224
     ------------------------------------------------------------------------------------------------------------------*/
5225
-    DayGrid.prototype.renderDates = function (dateProfile) {
5226
-        this.dateProfile = dateProfile;
5227
-        this.updateDayTable();
5228
-        this.renderGrid();
5229
-    };
5230
-    DayGrid.prototype.unrenderDates = function () {
5231
-        this.removeSegPopover();
5232
-    };
5233
-    // Renders the rows and columns into the component's `this.el`, which should already be assigned.
5234
-    DayGrid.prototype.renderGrid = function () {
5235
-        var view = this.view;
5236
-        var rowCnt = this.rowCnt;
5237
-        var colCnt = this.colCnt;
5238
-        var html = '';
5239
-        var row;
5240
-        var col;
5241
-        if (this.headContainerEl) {
5242
-            this.headContainerEl.html(this.renderHeadHtml());
5243
-        }
5244
-        for (row = 0; row < rowCnt; row++) {
5245
-            html += this.renderDayRowHtml(row, this.isRigid);
5246
-        }
5247
-        this.el.html(html);
5248
-        this.rowEls = this.el.find('.fc-row');
5249
-        this.cellEls = this.el.find('.fc-day, .fc-disabled-day');
5250
-        this.rowCoordCache = new CoordCache_1.default({
5251
-            els: this.rowEls,
5252
-            isVertical: true
5253
-        });
5254
-        this.colCoordCache = new CoordCache_1.default({
5255
-            els: this.cellEls.slice(0, this.colCnt),
5256
-            isHorizontal: true
5257
-        });
5258
-        // trigger dayRender with each cell's element
5259
-        for (row = 0; row < rowCnt; row++) {
5260
-            for (col = 0; col < colCnt; col++) {
5261
-                this.publiclyTrigger('dayRender', {
5262
-                    context: view,
5263
-                    args: [
5264
-                        this.getCellDate(row, col),
5265
-                        this.getCellEl(row, col),
5266
-                        view
5267
-                    ]
5268
-                });
5269
-            }
5270
-        }
5271
-    };
5272
-    // Generates the HTML for a single row, which is a div that wraps a table.
5273
-    // `row` is the row number.
5274
-    DayGrid.prototype.renderDayRowHtml = function (row, isRigid) {
5275
+    DayTableMixin.prototype.renderHeadHtml = function () {
5276
         var theme = this.view.calendar.theme;
5277
-        var classes = ['fc-row', 'fc-week', theme.getClass('dayRow')];
5278
-        if (isRigid) {
5279
-            classes.push('fc-rigid');
5280
-        }
5281
         return '' +
5282
-            '<div class="' + classes.join(' ') + '">' +
5283
-            '<div class="fc-bg">' +
5284
+            '<div class="fc-row ' + theme.getClass('headerRow') + '">' +
5285
             '<table class="' + theme.getClass('tableGrid') + '">' +
5286
-            this.renderBgTrHtml(row) +
5287
-            '</table>' +
5288
-            '</div>' +
5289
-            '<div class="fc-content-skeleton">' +
5290
-            '<table>' +
5291
-            (this.getIsNumbersVisible() ?
5292
-                '<thead>' +
5293
-                    this.renderNumberTrHtml(row) +
5294
-                    '</thead>' :
5295
-                '') +
5296
+            '<thead>' +
5297
+            this.renderHeadTrHtml() +
5298
+            '</thead>' +
5299
             '</table>' +
5300
-            '</div>' +
5301
             '</div>';
5302
     };
5303
-    DayGrid.prototype.getIsNumbersVisible = function () {
5304
-        return this.getIsDayNumbersVisible() || this.cellWeekNumbersVisible;
5305
-    };
5306
-    DayGrid.prototype.getIsDayNumbersVisible = function () {
5307
-        return this.rowCnt > 1;
5308
+    DayTableMixin.prototype.renderHeadIntroHtml = function () {
5309
+        return this.renderIntroHtml(); // fall back to generic
5310
     };
5311
-    /* Grid Number Rendering
5312
-    ------------------------------------------------------------------------------------------------------------------*/
5313
-    DayGrid.prototype.renderNumberTrHtml = function (row) {
5314
+    DayTableMixin.prototype.renderHeadTrHtml = function () {
5315
         return '' +
5316
             '<tr>' +
5317
-            (this.isRTL ? '' : this.renderNumberIntroHtml(row)) +
5318
-            this.renderNumberCellsHtml(row) +
5319
-            (this.isRTL ? this.renderNumberIntroHtml(row) : '') +
5320
+            (this.isRTL ? '' : this.renderHeadIntroHtml()) +
5321
+            this.renderHeadDateCellsHtml() +
5322
+            (this.isRTL ? this.renderHeadIntroHtml() : '') +
5323
             '</tr>';
5324
     };
5325
-    DayGrid.prototype.renderNumberIntroHtml = function (row) {
5326
-        return this.renderIntroHtml();
5327
-    };
5328
-    DayGrid.prototype.renderNumberCellsHtml = function (row) {
5329
+    DayTableMixin.prototype.renderHeadDateCellsHtml = function () {
5330
         var htmls = [];
5331
         var col;
5332
         var date;
5333
         for (col = 0; col < this.colCnt; col++) {
5334
-            date = this.getCellDate(row, col);
5335
-            htmls.push(this.renderNumberCellHtml(date));
5336
+            date = this.getCellDate(0, col);
5337
+            htmls.push(this.renderHeadDateCellHtml(date));
5338
         }
5339
         return htmls.join('');
5340
     };
5341
-    // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
5342
-    // The number row will only exist if either day numbers or week numbers are turned on.
5343
-    DayGrid.prototype.renderNumberCellHtml = function (date) {
5344
-        var view = this.view;
5345
-        var html = '';
5346
-        var isDateValid = this.dateProfile.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
5347
-        var isDayNumberVisible = this.getIsDayNumbersVisible() && isDateValid;
5348
-        var classes;
5349
-        var weekCalcFirstDoW;
5350
-        if (!isDayNumberVisible && !this.cellWeekNumbersVisible) {
5351
-            // no numbers in day cell (week number must be along the side)
5352
-            return '<td/>'; //  will create an empty space above events :(
5353
+    // TODO: when internalApiVersion, accept an object for HTML attributes
5354
+    // (colspan should be no different)
5355
+    DayTableMixin.prototype.renderHeadDateCellHtml = function (date, colspan, otherAttrs) {
5356
+        var t = this;
5357
+        var view = t.view;
5358
+        var isDateValid = t.dateProfile.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
5359
+        var classNames = [
5360
+            'fc-day-header',
5361
+            view.calendar.theme.getClass('widgetHeader')
5362
+        ];
5363
+        var innerHtml;
5364
+        if (typeof t.opt('columnHeaderHtml') === 'function') {
5365
+            innerHtml = t.opt('columnHeaderHtml')(date);
5366
         }
5367
-        classes = this.getDayClasses(date);
5368
-        classes.unshift('fc-day-top');
5369
-        if (this.cellWeekNumbersVisible) {
5370
-            // To determine the day of week number change under ISO, we cannot
5371
-            // rely on moment.js methods such as firstDayOfWeek() or weekday(),
5372
-            // because they rely on the locale's dow (possibly overridden by
5373
-            // our firstDay option), which may not be Monday. We cannot change
5374
-            // dow, because that would affect the calendar start day as well.
5375
-            if (date._locale._fullCalendar_weekCalc === 'ISO') {
5376
-                weekCalcFirstDoW = 1; // Monday by ISO 8601 definition
5377
-            }
5378
-            else {
5379
-                weekCalcFirstDoW = date._locale.firstDayOfWeek();
5380
-            }
5381
+        else if (typeof t.opt('columnHeaderText') === 'function') {
5382
+            innerHtml = util_1.htmlEscape(t.opt('columnHeaderText')(date));
5383
         }
5384
-        html += '<td class="' + classes.join(' ') + '"' +
5385
-            (isDateValid ?
5386
-                ' data-date="' + date.format() + '"' :
5387
-                '') +
5388
-            '>';
5389
-        if (this.cellWeekNumbersVisible && (date.day() === weekCalcFirstDoW)) {
5390
-            html += view.buildGotoAnchorHtml({ date: date, type: 'week' }, { 'class': 'fc-week-number' }, date.format('w') // inner HTML
5391
-            );
5392
+        else {
5393
+            innerHtml = util_1.htmlEscape(date.format(t.colHeadFormat));
5394
         }
5395
-        if (isDayNumberVisible) {
5396
-            html += view.buildGotoAnchorHtml(date, { 'class': 'fc-day-number' }, date.format('D') // inner HTML
5397
-            );
5398
+        // if only one row of days, the classNames on the header can represent the specific days beneath
5399
+        if (t.rowCnt === 1) {
5400
+            classNames = classNames.concat(
5401
+            // includes the day-of-week class
5402
+            // noThemeHighlight=true (don't highlight the header)
5403
+            t.getDayClasses(date, true));
5404
         }
5405
-        html += '</td>';
5406
-        return html;
5407
+        else {
5408
+            classNames.push('fc-' + util_1.dayIDs[date.day()]); // only add the day-of-week class
5409
+        }
5410
+        return '' +
5411
+            '<th class="' + classNames.join(' ') + '"' +
5412
+            ((isDateValid && t.rowCnt) === 1 ?
5413
+                ' data-date="' + date.format('YYYY-MM-DD') + '"' :
5414
+                '') +
5415
+            (colspan > 1 ?
5416
+                ' colspan="' + colspan + '"' :
5417
+                '') +
5418
+            (otherAttrs ?
5419
+                ' ' + otherAttrs :
5420
+                '') +
5421
+            '>' +
5422
+            (isDateValid ?
5423
+                // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff)
5424
+                view.buildGotoAnchorHtml({ date: date, forceOff: t.rowCnt > 1 || t.colCnt === 1 }, innerHtml) :
5425
+                // if not valid, display text, but no link
5426
+                innerHtml) +
5427
+            '</th>';
5428
     };
5429
-    /* Hit System
5430
+    /* Background Rendering
5431
     ------------------------------------------------------------------------------------------------------------------*/
5432
-    DayGrid.prototype.prepareHits = function () {
5433
-        this.colCoordCache.build();
5434
-        this.rowCoordCache.build();
5435
-        this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack
5436
-    };
5437
-    DayGrid.prototype.releaseHits = function () {
5438
-        this.colCoordCache.clear();
5439
-        this.rowCoordCache.clear();
5440
+    DayTableMixin.prototype.renderBgTrHtml = function (row) {
5441
+        return '' +
5442
+            '<tr>' +
5443
+            (this.isRTL ? '' : this.renderBgIntroHtml(row)) +
5444
+            this.renderBgCellsHtml(row) +
5445
+            (this.isRTL ? this.renderBgIntroHtml(row) : '') +
5446
+            '</tr>';
5447
     };
5448
-    DayGrid.prototype.queryHit = function (leftOffset, topOffset) {
5449
-        if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) {
5450
-            var col = this.colCoordCache.getHorizontalIndex(leftOffset);
5451
-            var row = this.rowCoordCache.getVerticalIndex(topOffset);
5452
-            if (row != null && col != null) {
5453
-                return this.getCellHit(row, col);
5454
-            }
5455
-        }
5456
+    DayTableMixin.prototype.renderBgIntroHtml = function (row) {
5457
+        return this.renderIntroHtml(); // fall back to generic
5458
     };
5459
-    DayGrid.prototype.getHitFootprint = function (hit) {
5460
-        var range = this.getCellRange(hit.row, hit.col);
5461
-        return new ComponentFootprint_1.default(new UnzonedRange_1.default(range.start, range.end), true // all-day?
5462
-        );
5463
+    DayTableMixin.prototype.renderBgCellsHtml = function (row) {
5464
+        var htmls = [];
5465
+        var col;
5466
+        var date;
5467
+        for (col = 0; col < this.colCnt; col++) {
5468
+            date = this.getCellDate(row, col);
5469
+            htmls.push(this.renderBgCellHtml(date));
5470
+        }
5471
+        return htmls.join('');
5472
     };
5473
-    DayGrid.prototype.getHitEl = function (hit) {
5474
-        return this.getCellEl(hit.row, hit.col);
5475
+    DayTableMixin.prototype.renderBgCellHtml = function (date, otherAttrs) {
5476
+        var t = this;
5477
+        var view = t.view;
5478
+        var isDateValid = t.dateProfile.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
5479
+        var classes = t.getDayClasses(date);
5480
+        classes.unshift('fc-day', view.calendar.theme.getClass('widgetContent'));
5481
+        return '<td class="' + classes.join(' ') + '"' +
5482
+            (isDateValid ?
5483
+                ' data-date="' + date.format('YYYY-MM-DD') + '"' : // if date has a time, won't format it
5484
+                '') +
5485
+            (otherAttrs ?
5486
+                ' ' + otherAttrs :
5487
+                '') +
5488
+            '></td>';
5489
     };
5490
-    /* Cell System
5491
+    /* Generic
5492
     ------------------------------------------------------------------------------------------------------------------*/
5493
-    // FYI: the first column is the leftmost column, regardless of date
5494
-    DayGrid.prototype.getCellHit = function (row, col) {
5495
-        return {
5496
-            row: row,
5497
-            col: col,
5498
-            component: this,
5499
-            left: this.colCoordCache.getLeftOffset(col),
5500
-            right: this.colCoordCache.getRightOffset(col),
5501
-            top: this.rowCoordCache.getTopOffset(row),
5502
-            bottom: this.rowCoordCache.getBottomOffset(row)
5503
-        };
5504
-    };
5505
-    DayGrid.prototype.getCellEl = function (row, col) {
5506
-        return this.cellEls.eq(row * this.colCnt + col);
5507
+    DayTableMixin.prototype.renderIntroHtml = function () {
5508
+        // Generates the default HTML intro for any row. User classes should override
5509
     };
5510
-    /* Event Rendering
5511
+    // TODO: a generic method for dealing with <tr>, RTL, intro
5512
+    // when increment internalApiVersion
5513
+    // wrapTr (scheduler)
5514
+    /* Utils
5515
     ------------------------------------------------------------------------------------------------------------------*/
5516
-    // Unrenders all events currently rendered on the grid
5517
-    DayGrid.prototype.executeEventUnrender = function () {
5518
-        this.removeSegPopover(); // removes the "more.." events popover
5519
-        _super.prototype.executeEventUnrender.call(this);
5520
+    // Applies the generic "intro" and "outro" HTML to the given cells.
5521
+    // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
5522
+    DayTableMixin.prototype.bookendCells = function (trEl) {
5523
+        var introHtml = this.renderIntroHtml();
5524
+        if (introHtml) {
5525
+            if (this.isRTL) {
5526
+                trEl.append(introHtml);
5527
+            }
5528
+            else {
5529
+                trEl.prepend(introHtml);
5530
+            }
5531
+        }
5532
     };
5533
-    // Retrieves all rendered segment objects currently rendered on the grid
5534
-    DayGrid.prototype.getOwnEventSegs = function () {
5535
-        // append the segments from the "more..." popover
5536
-        return _super.prototype.getOwnEventSegs.call(this).concat(this.popoverSegs || []);
5537
+    return DayTableMixin;
5538
+}(Mixin_1.default));
5539
+exports.default = DayTableMixin;
5540
+
5541
+
5542
+/***/ }),
5543
+/* 61 */
5544
+/***/ (function(module, exports) {
5545
+
5546
+Object.defineProperty(exports, "__esModule", { value: true });
5547
+var BusinessHourRenderer = /** @class */ (function () {
5548
+    /*
5549
+    component implements:
5550
+      - eventRangesToEventFootprints
5551
+      - eventFootprintsToSegs
5552
+    */
5553
+    function BusinessHourRenderer(component, fillRenderer) {
5554
+        this.component = component;
5555
+        this.fillRenderer = fillRenderer;
5556
+    }
5557
+    BusinessHourRenderer.prototype.render = function (businessHourGenerator) {
5558
+        var component = this.component;
5559
+        var unzonedRange = component._getDateProfile().activeUnzonedRange;
5560
+        var eventInstanceGroup = businessHourGenerator.buildEventInstanceGroup(component.hasAllDayBusinessHours, unzonedRange);
5561
+        var eventFootprints = eventInstanceGroup ?
5562
+            component.eventRangesToEventFootprints(eventInstanceGroup.sliceRenderRanges(unzonedRange)) :
5563
+            [];
5564
+        this.renderEventFootprints(eventFootprints);
5565
     };
5566
-    /* Event Drag Visualization
5567
-    ------------------------------------------------------------------------------------------------------------------*/
5568
-    // Renders a visual indication of an event or external element being dragged.
5569
-    // `eventLocation` has zoned start and end (optional)
5570
-    DayGrid.prototype.renderDrag = function (eventFootprints, seg, isTouch) {
5571
-        var i;
5572
-        for (i = 0; i < eventFootprints.length; i++) {
5573
-            this.renderHighlight(eventFootprints[i].componentFootprint);
5574
+    BusinessHourRenderer.prototype.renderEventFootprints = function (eventFootprints) {
5575
+        var segs = this.component.eventFootprintsToSegs(eventFootprints);
5576
+        this.renderSegs(segs);
5577
+        this.segs = segs;
5578
+    };
5579
+    BusinessHourRenderer.prototype.renderSegs = function (segs) {
5580
+        if (this.fillRenderer) {
5581
+            this.fillRenderer.renderSegs('businessHours', segs, {
5582
+                getClasses: function (seg) {
5583
+                    return ['fc-nonbusiness', 'fc-bgevent'];
5584
+                }
5585
+            });
5586
         }
5587
-        // render drags from OTHER components as helpers
5588
-        if (eventFootprints.length && seg && seg.component !== this) {
5589
-            this.helperRenderer.renderEventDraggingFootprints(eventFootprints, seg, isTouch);
5590
-            return true; // signal helpers rendered
5591
+    };
5592
+    BusinessHourRenderer.prototype.unrender = function () {
5593
+        if (this.fillRenderer) {
5594
+            this.fillRenderer.unrender('businessHours');
5595
         }
5596
+        this.segs = null;
5597
     };
5598
-    // Unrenders any visual indication of a hovering event
5599
-    DayGrid.prototype.unrenderDrag = function () {
5600
-        this.unrenderHighlight();
5601
-        this.helperRenderer.unrender();
5602
+    BusinessHourRenderer.prototype.getSegs = function () {
5603
+        return this.segs || [];
5604
     };
5605
-    /* Event Resize Visualization
5606
-    ------------------------------------------------------------------------------------------------------------------*/
5607
-    // Renders a visual indication of an event being resized
5608
-    DayGrid.prototype.renderEventResize = function (eventFootprints, seg, isTouch) {
5609
-        var i;
5610
-        for (i = 0; i < eventFootprints.length; i++) {
5611
-            this.renderHighlight(eventFootprints[i].componentFootprint);
5612
-        }
5613
-        this.helperRenderer.renderEventResizingFootprints(eventFootprints, seg, isTouch);
5614
+    return BusinessHourRenderer;
5615
+}());
5616
+exports.default = BusinessHourRenderer;
5617
+
5618
+
5619
+/***/ }),
5620
+/* 62 */
5621
+/***/ (function(module, exports, __webpack_require__) {
5622
+
5623
+Object.defineProperty(exports, "__esModule", { value: true });
5624
+var $ = __webpack_require__(3);
5625
+var util_1 = __webpack_require__(4);
5626
+var FillRenderer = /** @class */ (function () {
5627
+    function FillRenderer(component) {
5628
+        this.fillSegTag = 'div';
5629
+        this.component = component;
5630
+        this.elsByFill = {};
5631
+    }
5632
+    FillRenderer.prototype.renderFootprint = function (type, componentFootprint, props) {
5633
+        this.renderSegs(type, this.component.componentFootprintToSegs(componentFootprint), props);
5634
     };
5635
-    // Unrenders a visual indication of an event being resized
5636
-    DayGrid.prototype.unrenderEventResize = function () {
5637
-        this.unrenderHighlight();
5638
-        this.helperRenderer.unrender();
5639
+    FillRenderer.prototype.renderSegs = function (type, segs, props) {
5640
+        var els;
5641
+        segs = this.buildSegEls(type, segs, props); // assignes `.el` to each seg. returns successfully rendered segs
5642
+        els = this.attachSegEls(type, segs);
5643
+        if (els) {
5644
+            this.reportEls(type, els);
5645
+        }
5646
+        return segs;
5647
     };
5648
-    /* More+ Link Popover
5649
-    ------------------------------------------------------------------------------------------------------------------*/
5650
-    DayGrid.prototype.removeSegPopover = function () {
5651
-        if (this.segPopover) {
5652
-            this.segPopover.hide(); // in handler, will call segPopover's removeElement
5653
+    // Unrenders a specific type of fill that is currently rendered on the grid
5654
+    FillRenderer.prototype.unrender = function (type) {
5655
+        var el = this.elsByFill[type];
5656
+        if (el) {
5657
+            el.remove();
5658
+            delete this.elsByFill[type];
5659
         }
5660
     };
5661
-    // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
5662
-    // `levelLimit` can be false (don't limit), a number, or true (should be computed).
5663
-    DayGrid.prototype.limitRows = function (levelLimit) {
5664
-        var rowStructs = this.eventRenderer.rowStructs || [];
5665
-        var row; // row #
5666
-        var rowLevelLimit;
5667
-        for (row = 0; row < rowStructs.length; row++) {
5668
-            this.unlimitRow(row);
5669
-            if (!levelLimit) {
5670
-                rowLevelLimit = false;
5671
-            }
5672
-            else if (typeof levelLimit === 'number') {
5673
-                rowLevelLimit = levelLimit;
5674
-            }
5675
-            else {
5676
-                rowLevelLimit = this.computeRowLevelLimit(row);
5677
-            }
5678
-            if (rowLevelLimit !== false) {
5679
-                this.limitRow(row, rowLevelLimit);
5680
+    // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
5681
+    // Only returns segments that successfully rendered.
5682
+    FillRenderer.prototype.buildSegEls = function (type, segs, props) {
5683
+        var _this = this;
5684
+        var html = '';
5685
+        var renderedSegs = [];
5686
+        var i;
5687
+        if (segs.length) {
5688
+            // build a large concatenation of segment HTML
5689
+            for (i = 0; i < segs.length; i++) {
5690
+                html += this.buildSegHtml(type, segs[i], props);
5691
             }
5692
+            // Grab individual elements from the combined HTML string. Use each as the default rendering.
5693
+            // Then, compute the 'el' for each segment.
5694
+            $(html).each(function (i, node) {
5695
+                var seg = segs[i];
5696
+                var el = $(node);
5697
+                // allow custom filter methods per-type
5698
+                if (props.filterEl) {
5699
+                    el = props.filterEl(seg, el);
5700
+                }
5701
+                if (el) { // custom filters did not cancel the render
5702
+                    el = $(el); // allow custom filter to return raw DOM node
5703
+                    // correct element type? (would be bad if a non-TD were inserted into a table for example)
5704
+                    if (el.is(_this.fillSegTag)) {
5705
+                        seg.el = el;
5706
+                        renderedSegs.push(seg);
5707
+                    }
5708
+                }
5709
+            });
5710
         }
5711
+        return renderedSegs;
5712
     };
5713
-    // Computes the number of levels a row will accomodate without going outside its bounds.
5714
-    // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
5715
-    // `row` is the row number.
5716
-    DayGrid.prototype.computeRowLevelLimit = function (row) {
5717
-        var rowEl = this.rowEls.eq(row); // the containing "fake" row div
5718
-        var rowHeight = rowEl.height(); // TODO: cache somehow?
5719
-        var trEls = this.eventRenderer.rowStructs[row].tbodyEl.children();
5720
-        var i;
5721
-        var trEl;
5722
-        var trHeight;
5723
-        function iterInnerHeights(i, childNode) {
5724
-            trHeight = Math.max(trHeight, $(childNode).outerHeight());
5725
-        }
5726
-        // Reveal one level <tr> at a time and stop when we find one out of bounds
5727
-        for (i = 0; i < trEls.length; i++) {
5728
-            trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal)
5729
-            // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
5730
-            // so instead, find the tallest inner content element.
5731
-            trHeight = 0;
5732
-            trEl.find('> td > :first-child').each(iterInnerHeights);
5733
-            if (trEl.position().top + trHeight > rowHeight) {
5734
-                return i;
5735
-            }
5736
-        }
5737
-        return false; // should not limit at all
5738
+    // Builds the HTML needed for one fill segment. Generic enough to work with different types.
5739
+    FillRenderer.prototype.buildSegHtml = function (type, seg, props) {
5740
+        // custom hooks per-type
5741
+        var classes = props.getClasses ? props.getClasses(seg) : [];
5742
+        var css = util_1.cssToStr(props.getCss ? props.getCss(seg) : {});
5743
+        return '<' + this.fillSegTag +
5744
+            (classes.length ? ' class="' + classes.join(' ') + '"' : '') +
5745
+            (css ? ' style="' + css + '"' : '') +
5746
+            '></' + this.fillSegTag + '>';
5747
     };
5748
-    // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
5749
-    // `row` is the row number.
5750
-    // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
5751
-    DayGrid.prototype.limitRow = function (row, levelLimit) {
5752
-        var _this = this;
5753
-        var rowStruct = this.eventRenderer.rowStructs[row];
5754
-        var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
5755
-        var col = 0; // col #, left-to-right (not chronologically)
5756
-        var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
5757
-        var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
5758
-        var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
5759
-        var i;
5760
-        var seg;
5761
-        var segsBelow; // array of segment objects below `seg` in the current `col`
5762
-        var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
5763
-        var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
5764
-        var td;
5765
-        var rowspan;
5766
-        var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
5767
-        var j;
5768
-        var moreTd;
5769
-        var moreWrap;
5770
-        var moreLink;
5771
-        // Iterates through empty level cells and places "more" links inside if need be
5772
-        var emptyCellsUntil = function (endCol) {
5773
-            while (col < endCol) {
5774
-                segsBelow = _this.getCellSegs(row, col, levelLimit);
5775
-                if (segsBelow.length) {
5776
-                    td = cellMatrix[levelLimit - 1][col];
5777
-                    moreLink = _this.renderMoreLink(row, col, segsBelow);
5778
-                    moreWrap = $('<div/>').append(moreLink);
5779
-                    td.append(moreWrap);
5780
-                    moreNodes.push(moreWrap[0]);
5781
-                }
5782
-                col++;
5783
-            }
5784
-        };
5785
-        if (levelLimit && levelLimit < rowStruct.segLevels.length) {
5786
-            levelSegs = rowStruct.segLevels[levelLimit - 1];
5787
-            cellMatrix = rowStruct.cellMatrix;
5788
-            limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
5789
-                .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
5790
-            // iterate though segments in the last allowable level
5791
-            for (i = 0; i < levelSegs.length; i++) {
5792
-                seg = levelSegs[i];
5793
-                emptyCellsUntil(seg.leftCol); // process empty cells before the segment
5794
-                // determine *all* segments below `seg` that occupy the same columns
5795
-                colSegsBelow = [];
5796
-                totalSegsBelow = 0;
5797
-                while (col <= seg.rightCol) {
5798
-                    segsBelow = this.getCellSegs(row, col, levelLimit);
5799
-                    colSegsBelow.push(segsBelow);
5800
-                    totalSegsBelow += segsBelow.length;
5801
-                    col++;
5802
-                }
5803
-                if (totalSegsBelow) {
5804
-                    td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
5805
-                    rowspan = td.attr('rowspan') || 1;
5806
-                    segMoreNodes = [];
5807
-                    // make a replacement <td> for each column the segment occupies. will be one for each colspan
5808
-                    for (j = 0; j < colSegsBelow.length; j++) {
5809
-                        moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);
5810
-                        segsBelow = colSegsBelow[j];
5811
-                        moreLink = this.renderMoreLink(row, seg.leftCol + j, [seg].concat(segsBelow) // count seg as hidden too
5812
-                        );
5813
-                        moreWrap = $('<div/>').append(moreLink);
5814
-                        moreTd.append(moreWrap);
5815
-                        segMoreNodes.push(moreTd[0]);
5816
-                        moreNodes.push(moreTd[0]);
5817
-                    }
5818
-                    td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
5819
-                    limitedNodes.push(td[0]);
5820
-                }
5821
-            }
5822
-            emptyCellsUntil(this.colCnt); // finish off the level
5823
-            rowStruct.moreEls = $(moreNodes); // for easy undoing later
5824
-            rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
5825
-        }
5826
+    // Should return wrapping DOM structure
5827
+    FillRenderer.prototype.attachSegEls = function (type, segs) {
5828
+        // subclasses must implement
5829
     };
5830
-    // Reveals all levels and removes all "more"-related elements for a grid's row.
5831
-    // `row` is a row number.
5832
-    DayGrid.prototype.unlimitRow = function (row) {
5833
-        var rowStruct = this.eventRenderer.rowStructs[row];
5834
-        if (rowStruct.moreEls) {
5835
-            rowStruct.moreEls.remove();
5836
-            rowStruct.moreEls = null;
5837
+    FillRenderer.prototype.reportEls = function (type, nodes) {
5838
+        if (this.elsByFill[type]) {
5839
+            this.elsByFill[type] = this.elsByFill[type].add(nodes);
5840
         }
5841
-        if (rowStruct.limitedEls) {
5842
-            rowStruct.limitedEls.removeClass('fc-limited');
5843
-            rowStruct.limitedEls = null;
5844
+        else {
5845
+            this.elsByFill[type] = $(nodes);
5846
         }
5847
     };
5848
-    // Renders an <a> element that represents hidden event element for a cell.
5849
-    // Responsible for attaching click handler as well.
5850
-    DayGrid.prototype.renderMoreLink = function (row, col, hiddenSegs) {
5851
-        var _this = this;
5852
-        var view = this.view;
5853
-        return $('<a class="fc-more"/>')
5854
-            .text(this.getMoreLinkText(hiddenSegs.length))
5855
-            .on('click', function (ev) {
5856
-            var clickOption = _this.opt('eventLimitClick');
5857
-            var date = _this.getCellDate(row, col);
5858
-            var moreEl = $(ev.currentTarget);
5859
-            var dayEl = _this.getCellEl(row, col);
5860
-            var allSegs = _this.getCellSegs(row, col);
5861
-            // rescope the segments to be within the cell's date
5862
-            var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
5863
-            var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
5864
-            if (typeof clickOption === 'function') {
5865
-                // the returned value can be an atomic option
5866
-                clickOption = _this.publiclyTrigger('eventLimitClick', {
5867
-                    context: view,
5868
-                    args: [
5869
-                        {
5870
-                            date: date.clone(),
5871
-                            dayEl: dayEl,
5872
-                            moreEl: moreEl,
5873
-                            segs: reslicedAllSegs,
5874
-                            hiddenSegs: reslicedHiddenSegs
5875
-                        },
5876
-                        ev,
5877
-                        view
5878
-                    ]
5879
-                });
5880
-            }
5881
-            if (clickOption === 'popover') {
5882
-                _this.showSegPopover(row, col, moreEl, reslicedAllSegs);
5883
-            }
5884
-            else if (typeof clickOption === 'string') {
5885
-                view.calendar.zoomTo(date, clickOption);
5886
-            }
5887
-        });
5888
+    return FillRenderer;
5889
+}());
5890
+exports.default = FillRenderer;
5891
+
5892
+
5893
+/***/ }),
5894
+/* 63 */
5895
+/***/ (function(module, exports, __webpack_require__) {
5896
+
5897
+Object.defineProperty(exports, "__esModule", { value: true });
5898
+var SingleEventDef_1 = __webpack_require__(9);
5899
+var EventFootprint_1 = __webpack_require__(34);
5900
+var EventSource_1 = __webpack_require__(6);
5901
+var HelperRenderer = /** @class */ (function () {
5902
+    function HelperRenderer(component, eventRenderer) {
5903
+        this.view = component._getView();
5904
+        this.component = component;
5905
+        this.eventRenderer = eventRenderer;
5906
+    }
5907
+    HelperRenderer.prototype.renderComponentFootprint = function (componentFootprint) {
5908
+        this.renderEventFootprints([
5909
+            this.fabricateEventFootprint(componentFootprint)
5910
+        ]);
5911
     };
5912
-    // Reveals the popover that displays all events within a cell
5913
-    DayGrid.prototype.showSegPopover = function (row, col, moreLink, segs) {
5914
-        var _this = this;
5915
-        var view = this.view;
5916
-        var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
5917
-        var topEl; // the element we want to match the top coordinate of
5918
-        var options;
5919
-        if (this.rowCnt === 1) {
5920
-            topEl = view.el; // will cause the popover to cover any sort of header
5921
-        }
5922
-        else {
5923
-            topEl = this.rowEls.eq(row); // will align with top of row
5924
+    HelperRenderer.prototype.renderEventDraggingFootprints = function (eventFootprints, sourceSeg, isTouch) {
5925
+        this.renderEventFootprints(eventFootprints, sourceSeg, 'fc-dragging', isTouch ? null : this.view.opt('dragOpacity'));
5926
+    };
5927
+    HelperRenderer.prototype.renderEventResizingFootprints = function (eventFootprints, sourceSeg, isTouch) {
5928
+        this.renderEventFootprints(eventFootprints, sourceSeg, 'fc-resizing');
5929
+    };
5930
+    HelperRenderer.prototype.renderEventFootprints = function (eventFootprints, sourceSeg, extraClassNames, opacity) {
5931
+        var segs = this.component.eventFootprintsToSegs(eventFootprints);
5932
+        var classNames = 'fc-helper ' + (extraClassNames || '');
5933
+        var i;
5934
+        // assigns each seg's el and returns a subset of segs that were rendered
5935
+        segs = this.eventRenderer.renderFgSegEls(segs);
5936
+        for (i = 0; i < segs.length; i++) {
5937
+            segs[i].el.addClass(classNames);
5938
         }
5939
-        options = {
5940
-            className: 'fc-more-popover ' + view.calendar.theme.getClass('popover'),
5941
-            content: this.renderSegPopoverContent(row, col, segs),
5942
-            parentEl: view.el,
5943
-            top: topEl.offset().top,
5944
-            autoHide: true,
5945
-            viewportConstrain: this.opt('popoverViewportConstrain'),
5946
-            hide: function () {
5947
-                // kill everything when the popover is hidden
5948
-                // notify events to be removed
5949
-                if (_this.popoverSegs) {
5950
-                    _this.triggerBeforeEventSegsDestroyed(_this.popoverSegs);
5951
-                }
5952
-                _this.segPopover.removeElement();
5953
-                _this.segPopover = null;
5954
-                _this.popoverSegs = null;
5955
+        if (opacity != null) {
5956
+            for (i = 0; i < segs.length; i++) {
5957
+                segs[i].el.css('opacity', opacity);
5958
             }
5959
-        };
5960
-        // Determine horizontal coordinate.
5961
-        // We use the moreWrap instead of the <td> to avoid border confusion.
5962
-        if (this.isRTL) {
5963
-            options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
5964
         }
5965
-        else {
5966
-            options.left = moreWrap.offset().left - 1; // -1 to be over cell border
5967
+        this.helperEls = this.renderSegs(segs, sourceSeg);
5968
+    };
5969
+    /*
5970
+    Must return all mock event elements
5971
+    */
5972
+    HelperRenderer.prototype.renderSegs = function (segs, sourceSeg) {
5973
+        // Subclasses must implement
5974
+    };
5975
+    HelperRenderer.prototype.unrender = function () {
5976
+        if (this.helperEls) {
5977
+            this.helperEls.remove();
5978
+            this.helperEls = null;
5979
         }
5980
-        this.segPopover = new Popover_1.default(options);
5981
-        this.segPopover.show();
5982
-        // the popover doesn't live within the grid's container element, and thus won't get the event
5983
-        // delegated-handlers for free. attach event-related handlers to the popover.
5984
-        this.bindAllSegHandlersToEl(this.segPopover.el);
5985
-        this.triggerAfterEventSegsRendered(segs);
5986
     };
5987
-    // Builds the inner DOM contents of the segment popover
5988
-    DayGrid.prototype.renderSegPopoverContent = function (row, col, segs) {
5989
-        var view = this.view;
5990
-        var theme = view.calendar.theme;
5991
-        var title = this.getCellDate(row, col).format(this.opt('dayPopoverFormat'));
5992
-        var content = $('<div class="fc-header ' + theme.getClass('popoverHeader') + '">' +
5993
-            '<span class="fc-close ' + theme.getIconClass('close') + '"></span>' +
5994
-            '<span class="fc-title">' +
5995
-            util_1.htmlEscape(title) +
5996
-            '</span>' +
5997
-            '<div class="fc-clear"/>' +
5998
-            '</div>' +
5999
-            '<div class="fc-body ' + theme.getClass('popoverContent') + '">' +
6000
-            '<div class="fc-event-container"></div>' +
6001
-            '</div>');
6002
-        var segContainer = content.find('.fc-event-container');
6003
-        var i;
6004
-        // render each seg's `el` and only return the visible segs
6005
-        segs = this.eventRenderer.renderFgSegEls(segs, true); // disableResizing=true
6006
-        this.popoverSegs = segs;
6007
-        for (i = 0; i < segs.length; i++) {
6008
-            // because segments in the popover are not part of a grid coordinate system, provide a hint to any
6009
-            // grids that want to do drag-n-drop about which cell it came from
6010
-            this.hitsNeeded();
6011
-            segs[i].hit = this.getCellHit(row, col);
6012
-            this.hitsNotNeeded();
6013
-            segContainer.append(segs[i].el);
6014
+    HelperRenderer.prototype.fabricateEventFootprint = function (componentFootprint) {
6015
+        var calendar = this.view.calendar;
6016
+        var eventDateProfile = calendar.footprintToDateProfile(componentFootprint);
6017
+        var dummyEvent = new SingleEventDef_1.default(new EventSource_1.default(calendar));
6018
+        var dummyInstance;
6019
+        dummyEvent.dateProfile = eventDateProfile;
6020
+        dummyInstance = dummyEvent.buildInstance();
6021
+        return new EventFootprint_1.default(componentFootprint, dummyEvent, dummyInstance);
6022
+    };
6023
+    return HelperRenderer;
6024
+}());
6025
+exports.default = HelperRenderer;
6026
+
6027
+
6028
+/***/ }),
6029
+/* 64 */
6030
+/***/ (function(module, exports, __webpack_require__) {
6031
+
6032
+Object.defineProperty(exports, "__esModule", { value: true });
6033
+var tslib_1 = __webpack_require__(2);
6034
+var GlobalEmitter_1 = __webpack_require__(23);
6035
+var Interaction_1 = __webpack_require__(14);
6036
+var EventPointing = /** @class */ (function (_super) {
6037
+    tslib_1.__extends(EventPointing, _super);
6038
+    function EventPointing() {
6039
+        return _super !== null && _super.apply(this, arguments) || this;
6040
+    }
6041
+    /*
6042
+    component must implement:
6043
+      - publiclyTrigger
6044
+    */
6045
+    EventPointing.prototype.bindToEl = function (el) {
6046
+        var component = this.component;
6047
+        component.bindSegHandlerToEl(el, 'click', this.handleClick.bind(this));
6048
+        component.bindSegHandlerToEl(el, 'mouseenter', this.handleMouseover.bind(this));
6049
+        component.bindSegHandlerToEl(el, 'mouseleave', this.handleMouseout.bind(this));
6050
+    };
6051
+    EventPointing.prototype.handleClick = function (seg, ev) {
6052
+        var res = this.component.publiclyTrigger('eventClick', {
6053
+            context: seg.el[0],
6054
+            args: [seg.footprint.getEventLegacy(), ev, this.view]
6055
+        });
6056
+        if (res === false) {
6057
+            ev.preventDefault();
6058
         }
6059
-        return content;
6060
     };
6061
-    // Given the events within an array of segment objects, reslice them to be in a single day
6062
-    DayGrid.prototype.resliceDaySegs = function (segs, dayDate) {
6063
-        var dayStart = dayDate.clone();
6064
-        var dayEnd = dayStart.clone().add(1, 'days');
6065
-        var dayRange = new UnzonedRange_1.default(dayStart, dayEnd);
6066
-        var newSegs = [];
6067
-        var i;
6068
-        var seg;
6069
-        var slicedRange;
6070
-        for (i = 0; i < segs.length; i++) {
6071
-            seg = segs[i];
6072
-            slicedRange = seg.footprint.componentFootprint.unzonedRange.intersect(dayRange);
6073
-            if (slicedRange) {
6074
-                newSegs.push($.extend({}, seg, {
6075
-                    footprint: new EventFootprint_1.default(new ComponentFootprint_1.default(slicedRange, seg.footprint.componentFootprint.isAllDay), seg.footprint.eventDef, seg.footprint.eventInstance),
6076
-                    isStart: seg.isStart && slicedRange.isStart,
6077
-                    isEnd: seg.isEnd && slicedRange.isEnd
6078
-                }));
6079
+    // Updates internal state and triggers handlers for when an event element is moused over
6080
+    EventPointing.prototype.handleMouseover = function (seg, ev) {
6081
+        if (!GlobalEmitter_1.default.get().shouldIgnoreMouse() &&
6082
+            !this.mousedOverSeg) {
6083
+            this.mousedOverSeg = seg;
6084
+            // TODO: move to EventSelecting's responsibility
6085
+            if (this.view.isEventDefResizable(seg.footprint.eventDef)) {
6086
+                seg.el.addClass('fc-allow-mouse-resize');
6087
             }
6088
+            this.component.publiclyTrigger('eventMouseover', {
6089
+                context: seg.el[0],
6090
+                args: [seg.footprint.getEventLegacy(), ev, this.view]
6091
+            });
6092
         }
6093
-        // force an order because eventsToSegs doesn't guarantee one
6094
-        // TODO: research if still needed
6095
-        this.eventRenderer.sortEventSegs(newSegs);
6096
-        return newSegs;
6097
     };
6098
-    // Generates the text that should be inside a "more" link, given the number of events it represents
6099
-    DayGrid.prototype.getMoreLinkText = function (num) {
6100
-        var opt = this.opt('eventLimitText');
6101
-        if (typeof opt === 'function') {
6102
-            return opt(num);
6103
-        }
6104
-        else {
6105
-            return '+' + num + ' ' + opt;
6106
+    // Updates internal state and triggers handlers for when an event element is moused out.
6107
+    // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
6108
+    EventPointing.prototype.handleMouseout = function (seg, ev) {
6109
+        if (this.mousedOverSeg) {
6110
+            this.mousedOverSeg = null;
6111
+            // TODO: move to EventSelecting's responsibility
6112
+            if (this.view.isEventDefResizable(seg.footprint.eventDef)) {
6113
+                seg.el.removeClass('fc-allow-mouse-resize');
6114
+            }
6115
+            this.component.publiclyTrigger('eventMouseout', {
6116
+                context: seg.el[0],
6117
+                args: [
6118
+                    seg.footprint.getEventLegacy(),
6119
+                    ev || {},
6120
+                    this.view
6121
+                ]
6122
+            });
6123
         }
6124
     };
6125
-    // Returns segments within a given cell.
6126
-    // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
6127
-    DayGrid.prototype.getCellSegs = function (row, col, startLevel) {
6128
-        var segMatrix = this.eventRenderer.rowStructs[row].segMatrix;
6129
-        var level = startLevel || 0;
6130
-        var segs = [];
6131
-        var seg;
6132
-        while (level < segMatrix.length) {
6133
-            seg = segMatrix[level][col];
6134
-            if (seg) {
6135
-                segs.push(seg);
6136
-            }
6137
-            level++;
6138
+    EventPointing.prototype.end = function () {
6139
+        if (this.mousedOverSeg) {
6140
+            this.handleMouseout(this.mousedOverSeg);
6141
         }
6142
-        return segs;
6143
     };
6144
-    return DayGrid;
6145
-}(InteractiveDateComponent_1.default));
6146
-exports.default = DayGrid;
6147
-DayGrid.prototype.eventRendererClass = DayGridEventRenderer_1.default;
6148
-DayGrid.prototype.businessHourRendererClass = BusinessHourRenderer_1.default;
6149
-DayGrid.prototype.helperRendererClass = DayGridHelperRenderer_1.default;
6150
-DayGrid.prototype.fillRendererClass = DayGridFillRenderer_1.default;
6151
-StandardInteractionsMixin_1.default.mixInto(DayGrid);
6152
-DayTableMixin_1.default.mixInto(DayGrid);
6153
+    return EventPointing;
6154
+}(Interaction_1.default));
6155
+exports.default = EventPointing;
6156
 
6157
 
6158
 /***/ }),
6159
-/* 62 */
6160
+/* 65 */
6161
+/***/ (function(module, exports, __webpack_require__) {
6162
+
6163
+Object.defineProperty(exports, "__esModule", { value: true });
6164
+var tslib_1 = __webpack_require__(2);
6165
+var Mixin_1 = __webpack_require__(15);
6166
+var DateClicking_1 = __webpack_require__(237);
6167
+var DateSelecting_1 = __webpack_require__(236);
6168
+var EventPointing_1 = __webpack_require__(64);
6169
+var EventDragging_1 = __webpack_require__(235);
6170
+var EventResizing_1 = __webpack_require__(234);
6171
+var ExternalDropping_1 = __webpack_require__(233);
6172
+var StandardInteractionsMixin = /** @class */ (function (_super) {
6173
+    tslib_1.__extends(StandardInteractionsMixin, _super);
6174
+    function StandardInteractionsMixin() {
6175
+        return _super !== null && _super.apply(this, arguments) || this;
6176
+    }
6177
+    return StandardInteractionsMixin;
6178
+}(Mixin_1.default));
6179
+exports.default = StandardInteractionsMixin;
6180
+StandardInteractionsMixin.prototype.dateClickingClass = DateClicking_1.default;
6181
+StandardInteractionsMixin.prototype.dateSelectingClass = DateSelecting_1.default;
6182
+StandardInteractionsMixin.prototype.eventPointingClass = EventPointing_1.default;
6183
+StandardInteractionsMixin.prototype.eventDraggingClass = EventDragging_1.default;
6184
+StandardInteractionsMixin.prototype.eventResizingClass = EventResizing_1.default;
6185
+StandardInteractionsMixin.prototype.externalDroppingClass = ExternalDropping_1.default;
6186
+
6187
+
6188
+/***/ }),
6189
+/* 66 */
6190
 /***/ (function(module, exports, __webpack_require__) {
6191
 
6192
 Object.defineProperty(exports, "__esModule", { value: true });
6193
 var tslib_1 = __webpack_require__(2);
6194
 var $ = __webpack_require__(3);
6195
 var util_1 = __webpack_require__(4);
6196
-var Scroller_1 = __webpack_require__(39);
6197
-var View_1 = __webpack_require__(41);
6198
-var BasicViewDateProfileGenerator_1 = __webpack_require__(228);
6199
-var DayGrid_1 = __webpack_require__(61);
6200
-/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
6201
+var CoordCache_1 = __webpack_require__(58);
6202
+var Popover_1 = __webpack_require__(227);
6203
+var UnzonedRange_1 = __webpack_require__(5);
6204
+var ComponentFootprint_1 = __webpack_require__(12);
6205
+var EventFootprint_1 = __webpack_require__(34);
6206
+var BusinessHourRenderer_1 = __webpack_require__(61);
6207
+var StandardInteractionsMixin_1 = __webpack_require__(65);
6208
+var InteractiveDateComponent_1 = __webpack_require__(42);
6209
+var DayTableMixin_1 = __webpack_require__(60);
6210
+var DayGridEventRenderer_1 = __webpack_require__(243);
6211
+var DayGridHelperRenderer_1 = __webpack_require__(244);
6212
+var DayGridFillRenderer_1 = __webpack_require__(245);
6213
+/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
6214
 ----------------------------------------------------------------------------------------------------------------------*/
6215
-// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
6216
-// It is responsible for managing width/height.
6217
-var BasicView = /** @class */ (function (_super) {
6218
-    tslib_1.__extends(BasicView, _super);
6219
-    function BasicView(calendar, viewSpec) {
6220
-        var _this = _super.call(this, calendar, viewSpec) || this;
6221
-        _this.dayGrid = _this.instantiateDayGrid();
6222
-        _this.dayGrid.isRigid = _this.hasRigidRows();
6223
-        if (_this.opt('weekNumbers')) {
6224
-            if (_this.opt('weekNumbersWithinDays')) {
6225
-                _this.dayGrid.cellWeekNumbersVisible = true;
6226
-                _this.dayGrid.colWeekNumbersVisible = false;
6227
+var DayGrid = /** @class */ (function (_super) {
6228
+    tslib_1.__extends(DayGrid, _super);
6229
+    function DayGrid(view) {
6230
+        var _this = _super.call(this, view) || this;
6231
+        _this.cellWeekNumbersVisible = false; // display week numbers in day cell?
6232
+        _this.bottomCoordPadding = 0; // hack for extending the hit area for the last row of the coordinate grid
6233
+        // isRigid determines whether the individual rows should ignore the contents and be a constant height.
6234
+        // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
6235
+        _this.isRigid = false;
6236
+        _this.hasAllDayBusinessHours = true;
6237
+        return _this;
6238
+    }
6239
+    // Slices up the given span (unzoned start/end with other misc data) into an array of segments
6240
+    DayGrid.prototype.componentFootprintToSegs = function (componentFootprint) {
6241
+        var segs = this.sliceRangeByRow(componentFootprint.unzonedRange);
6242
+        var i;
6243
+        var seg;
6244
+        for (i = 0; i < segs.length; i++) {
6245
+            seg = segs[i];
6246
+            if (this.isRTL) {
6247
+                seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex;
6248
+                seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex;
6249
             }
6250
             else {
6251
-                _this.dayGrid.cellWeekNumbersVisible = false;
6252
-                _this.dayGrid.colWeekNumbersVisible = true;
6253
+                seg.leftCol = seg.firstRowDayIndex;
6254
+                seg.rightCol = seg.lastRowDayIndex;
6255
             }
6256
         }
6257
-        _this.addChild(_this.dayGrid);
6258
-        _this.scroller = new Scroller_1.default({
6259
-            overflowX: 'hidden',
6260
-            overflowY: 'auto'
6261
-        });
6262
-        return _this;
6263
-    }
6264
-    // Generates the DayGrid object this view needs. Draws from this.dayGridClass
6265
-    BasicView.prototype.instantiateDayGrid = function () {
6266
-        // generate a subclass on the fly with BasicView-specific behavior
6267
-        // TODO: cache this subclass
6268
-        var subclass = makeDayGridSubclass(this.dayGridClass);
6269
-        return new subclass(this);
6270
-    };
6271
-    BasicView.prototype.executeDateRender = function (dateProfile) {
6272
-        this.dayGrid.breakOnWeeks = /year|month|week/.test(dateProfile.currentRangeUnit);
6273
-        _super.prototype.executeDateRender.call(this, dateProfile);
6274
+        return segs;
6275
     };
6276
-    BasicView.prototype.renderSkeleton = function () {
6277
-        var dayGridContainerEl;
6278
-        var dayGridEl;
6279
-        this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml());
6280
-        this.scroller.render();
6281
-        dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container');
6282
-        dayGridEl = $('<div class="fc-day-grid" />').appendTo(dayGridContainerEl);
6283
-        this.el.find('.fc-body > tr > td').append(dayGridContainerEl);
6284
-        this.dayGrid.headContainerEl = this.el.find('.fc-head-container');
6285
-        this.dayGrid.setElement(dayGridEl);
6286
+    /* Date Rendering
6287
+    ------------------------------------------------------------------------------------------------------------------*/
6288
+    DayGrid.prototype.renderDates = function (dateProfile) {
6289
+        this.dateProfile = dateProfile;
6290
+        this.updateDayTable();
6291
+        this.renderGrid();
6292
     };
6293
-    BasicView.prototype.unrenderSkeleton = function () {
6294
-        this.dayGrid.removeElement();
6295
-        this.scroller.destroy();
6296
+    DayGrid.prototype.unrenderDates = function () {
6297
+        this.removeSegPopover();
6298
     };
6299
-    // Builds the HTML skeleton for the view.
6300
-    // The day-grid component will render inside of a container defined by this HTML.
6301
-    BasicView.prototype.renderSkeletonHtml = function () {
6302
-        var theme = this.calendar.theme;
6303
+    // Renders the rows and columns into the component's `this.el`, which should already be assigned.
6304
+    DayGrid.prototype.renderGrid = function () {
6305
+        var view = this.view;
6306
+        var rowCnt = this.rowCnt;
6307
+        var colCnt = this.colCnt;
6308
+        var html = '';
6309
+        var row;
6310
+        var col;
6311
+        if (this.headContainerEl) {
6312
+            this.headContainerEl.html(this.renderHeadHtml());
6313
+        }
6314
+        for (row = 0; row < rowCnt; row++) {
6315
+            html += this.renderDayRowHtml(row, this.isRigid);
6316
+        }
6317
+        this.el.html(html);
6318
+        this.rowEls = this.el.find('.fc-row');
6319
+        this.cellEls = this.el.find('.fc-day, .fc-disabled-day');
6320
+        this.rowCoordCache = new CoordCache_1.default({
6321
+            els: this.rowEls,
6322
+            isVertical: true
6323
+        });
6324
+        this.colCoordCache = new CoordCache_1.default({
6325
+            els: this.cellEls.slice(0, this.colCnt),
6326
+            isHorizontal: true
6327
+        });
6328
+        // trigger dayRender with each cell's element
6329
+        for (row = 0; row < rowCnt; row++) {
6330
+            for (col = 0; col < colCnt; col++) {
6331
+                this.publiclyTrigger('dayRender', {
6332
+                    context: view,
6333
+                    args: [
6334
+                        this.getCellDate(row, col),
6335
+                        this.getCellEl(row, col),
6336
+                        view
6337
+                    ]
6338
+                });
6339
+            }
6340
+        }
6341
+    };
6342
+    // Generates the HTML for a single row, which is a div that wraps a table.
6343
+    // `row` is the row number.
6344
+    DayGrid.prototype.renderDayRowHtml = function (row, isRigid) {
6345
+        var theme = this.view.calendar.theme;
6346
+        var classes = ['fc-row', 'fc-week', theme.getClass('dayRow')];
6347
+        if (isRigid) {
6348
+            classes.push('fc-rigid');
6349
+        }
6350
         return '' +
6351
+            '<div class="' + classes.join(' ') + '">' +
6352
+            '<div class="fc-bg">' +
6353
             '<table class="' + theme.getClass('tableGrid') + '">' +
6354
-            (this.opt('columnHeader') ?
6355
-                '<thead class="fc-head">' +
6356
-                    '<tr>' +
6357
-                    '<td class="fc-head-container ' + theme.getClass('widgetHeader') + '">&nbsp;</td>' +
6358
-                    '</tr>' +
6359
+            this.renderBgTrHtml(row) +
6360
+            '</table>' +
6361
+            '</div>' +
6362
+            '<div class="fc-content-skeleton">' +
6363
+            '<table>' +
6364
+            (this.getIsNumbersVisible() ?
6365
+                '<thead>' +
6366
+                    this.renderNumberTrHtml(row) +
6367
                     '</thead>' :
6368
                 '') +
6369
-            '<tbody class="fc-body">' +
6370
-            '<tr>' +
6371
-            '<td class="' + theme.getClass('widgetContent') + '"></td>' +
6372
-            '</tr>' +
6373
-            '</tbody>' +
6374
-            '</table>';
6375
+            '</table>' +
6376
+            '</div>' +
6377
+            '</div>';
6378
     };
6379
-    // Generates an HTML attribute string for setting the width of the week number column, if it is known
6380
-    BasicView.prototype.weekNumberStyleAttr = function () {
6381
-        if (this.weekNumberWidth != null) {
6382
-            return 'style="width:' + this.weekNumberWidth + 'px"';
6383
-        }
6384
-        return '';
6385
+    DayGrid.prototype.getIsNumbersVisible = function () {
6386
+        return this.getIsDayNumbersVisible() || this.cellWeekNumbersVisible;
6387
     };
6388
-    // Determines whether each row should have a constant height
6389
-    BasicView.prototype.hasRigidRows = function () {
6390
-        var eventLimit = this.opt('eventLimit');
6391
-        return eventLimit && typeof eventLimit !== 'number';
6392
+    DayGrid.prototype.getIsDayNumbersVisible = function () {
6393
+        return this.rowCnt > 1;
6394
     };
6395
-    /* Dimensions
6396
+    /* Grid Number Rendering
6397
     ------------------------------------------------------------------------------------------------------------------*/
6398
-    // Refreshes the horizontal dimensions of the view
6399
-    BasicView.prototype.updateSize = function (totalHeight, isAuto, isResize) {
6400
-        var eventLimit = this.opt('eventLimit');
6401
-        var headRowEl = this.dayGrid.headContainerEl.find('.fc-row');
6402
-        var scrollerHeight;
6403
-        var scrollbarWidths;
6404
-        // hack to give the view some height prior to dayGrid's columns being rendered
6405
-        // TODO: separate setting height from scroller VS dayGrid.
6406
-        if (!this.dayGrid.rowEls) {
6407
-            if (!isAuto) {
6408
-                scrollerHeight = this.computeScrollerHeight(totalHeight);
6409
-                this.scroller.setHeight(scrollerHeight);
6410
-            }
6411
-            return;
6412
+    DayGrid.prototype.renderNumberTrHtml = function (row) {
6413
+        return '' +
6414
+            '<tr>' +
6415
+            (this.isRTL ? '' : this.renderNumberIntroHtml(row)) +
6416
+            this.renderNumberCellsHtml(row) +
6417
+            (this.isRTL ? this.renderNumberIntroHtml(row) : '') +
6418
+            '</tr>';
6419
+    };
6420
+    DayGrid.prototype.renderNumberIntroHtml = function (row) {
6421
+        return this.renderIntroHtml();
6422
+    };
6423
+    DayGrid.prototype.renderNumberCellsHtml = function (row) {
6424
+        var htmls = [];
6425
+        var col;
6426
+        var date;
6427
+        for (col = 0; col < this.colCnt; col++) {
6428
+            date = this.getCellDate(row, col);
6429
+            htmls.push(this.renderNumberCellHtml(date));
6430
         }
6431
-        _super.prototype.updateSize.call(this, totalHeight, isAuto, isResize);
6432
-        if (this.dayGrid.colWeekNumbersVisible) {
6433
-            // Make sure all week number cells running down the side have the same width.
6434
-            // Record the width for cells created later.
6435
-            this.weekNumberWidth = util_1.matchCellWidths(this.el.find('.fc-week-number'));
6436
+        return htmls.join('');
6437
+    };
6438
+    // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
6439
+    // The number row will only exist if either day numbers or week numbers are turned on.
6440
+    DayGrid.prototype.renderNumberCellHtml = function (date) {
6441
+        var view = this.view;
6442
+        var html = '';
6443
+        var isDateValid = this.dateProfile.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
6444
+        var isDayNumberVisible = this.getIsDayNumbersVisible() && isDateValid;
6445
+        var classes;
6446
+        var weekCalcFirstDoW;
6447
+        if (!isDayNumberVisible && !this.cellWeekNumbersVisible) {
6448
+            // no numbers in day cell (week number must be along the side)
6449
+            return '<td></td>'; //  will create an empty space above events :(
6450
         }
6451
-        // reset all heights to be natural
6452
-        this.scroller.clear();
6453
-        util_1.uncompensateScroll(headRowEl);
6454
-        this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
6455
-        // is the event limit a constant level number?
6456
-        if (eventLimit && typeof eventLimit === 'number') {
6457
-            this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
6458
+        classes = this.getDayClasses(date);
6459
+        classes.unshift('fc-day-top');
6460
+        if (this.cellWeekNumbersVisible) {
6461
+            // To determine the day of week number change under ISO, we cannot
6462
+            // rely on moment.js methods such as firstDayOfWeek() or weekday(),
6463
+            // because they rely on the locale's dow (possibly overridden by
6464
+            // our firstDay option), which may not be Monday. We cannot change
6465
+            // dow, because that would affect the calendar start day as well.
6466
+            if (date._locale._fullCalendar_weekCalc === 'ISO') {
6467
+                weekCalcFirstDoW = 1; // Monday by ISO 8601 definition
6468
+            }
6469
+            else {
6470
+                weekCalcFirstDoW = date._locale.firstDayOfWeek();
6471
+            }
6472
         }
6473
-        // distribute the height to the rows
6474
-        // (totalHeight is a "recommended" value if isAuto)
6475
-        scrollerHeight = this.computeScrollerHeight(totalHeight);
6476
-        this.setGridHeight(scrollerHeight, isAuto);
6477
-        // is the event limit dynamically calculated?
6478
-        if (eventLimit && typeof eventLimit !== 'number') {
6479
-            this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
6480
+        html += '<td class="' + classes.join(' ') + '"' +
6481
+            (isDateValid ?
6482
+                ' data-date="' + date.format() + '"' :
6483
+                '') +
6484
+            '>';
6485
+        if (this.cellWeekNumbersVisible && (date.day() === weekCalcFirstDoW)) {
6486
+            html += view.buildGotoAnchorHtml({ date: date, type: 'week' }, { 'class': 'fc-week-number' }, date.format('w') // inner HTML
6487
+            );
6488
         }
6489
-        if (!isAuto) {
6490
-            this.scroller.setHeight(scrollerHeight);
6491
-            scrollbarWidths = this.scroller.getScrollbarWidths();
6492
-            if (scrollbarWidths.left || scrollbarWidths.right) {
6493
-                util_1.compensateScroll(headRowEl, scrollbarWidths);
6494
-                // doing the scrollbar compensation might have created text overflow which created more height. redo
6495
-                scrollerHeight = this.computeScrollerHeight(totalHeight);
6496
-                this.scroller.setHeight(scrollerHeight);
6497
-            }
6498
-            // guarantees the same scrollbar widths
6499
-            this.scroller.lockOverflow(scrollbarWidths);
6500
+        if (isDayNumberVisible) {
6501
+            html += view.buildGotoAnchorHtml(date, { 'class': 'fc-day-number' }, date.format('D') // inner HTML
6502
+            );
6503
         }
6504
+        html += '</td>';
6505
+        return html;
6506
     };
6507
-    // given a desired total height of the view, returns what the height of the scroller should be
6508
-    BasicView.prototype.computeScrollerHeight = function (totalHeight) {
6509
-        return totalHeight -
6510
-            util_1.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
6511
+    /* Hit System
6512
+    ------------------------------------------------------------------------------------------------------------------*/
6513
+    DayGrid.prototype.prepareHits = function () {
6514
+        this.colCoordCache.build();
6515
+        this.rowCoordCache.build();
6516
+        this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack
6517
     };
6518
-    // Sets the height of just the DayGrid component in this view
6519
-    BasicView.prototype.setGridHeight = function (height, isAuto) {
6520
-        if (isAuto) {
6521
-            util_1.undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
6522
-        }
6523
-        else {
6524
-            util_1.distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
6525
+    DayGrid.prototype.releaseHits = function () {
6526
+        this.colCoordCache.clear();
6527
+        this.rowCoordCache.clear();
6528
+    };
6529
+    DayGrid.prototype.queryHit = function (leftOffset, topOffset) {
6530
+        if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) {
6531
+            var col = this.colCoordCache.getHorizontalIndex(leftOffset);
6532
+            var row = this.rowCoordCache.getVerticalIndex(topOffset);
6533
+            if (row != null && col != null) {
6534
+                return this.getCellHit(row, col);
6535
+            }
6536
         }
6537
     };
6538
-    /* Scroll
6539
+    DayGrid.prototype.getHitFootprint = function (hit) {
6540
+        var range = this.getCellRange(hit.row, hit.col);
6541
+        return new ComponentFootprint_1.default(new UnzonedRange_1.default(range.start, range.end), true // all-day?
6542
+        );
6543
+    };
6544
+    DayGrid.prototype.getHitEl = function (hit) {
6545
+        return this.getCellEl(hit.row, hit.col);
6546
+    };
6547
+    /* Cell System
6548
     ------------------------------------------------------------------------------------------------------------------*/
6549
-    BasicView.prototype.computeInitialDateScroll = function () {
6550
-        return { top: 0 };
6551
+    // FYI: the first column is the leftmost column, regardless of date
6552
+    DayGrid.prototype.getCellHit = function (row, col) {
6553
+        return {
6554
+            row: row,
6555
+            col: col,
6556
+            component: this,
6557
+            left: this.colCoordCache.getLeftOffset(col),
6558
+            right: this.colCoordCache.getRightOffset(col),
6559
+            top: this.rowCoordCache.getTopOffset(row),
6560
+            bottom: this.rowCoordCache.getBottomOffset(row)
6561
+        };
6562
     };
6563
-    BasicView.prototype.queryDateScroll = function () {
6564
-        return { top: this.scroller.getScrollTop() };
6565
+    DayGrid.prototype.getCellEl = function (row, col) {
6566
+        return this.cellEls.eq(row * this.colCnt + col);
6567
     };
6568
-    BasicView.prototype.applyDateScroll = function (scroll) {
6569
-        if (scroll.top !== undefined) {
6570
-            this.scroller.setScrollTop(scroll.top);
6571
-        }
6572
+    /* Event Rendering
6573
+    ------------------------------------------------------------------------------------------------------------------*/
6574
+    // Unrenders all events currently rendered on the grid
6575
+    DayGrid.prototype.executeEventUnrender = function () {
6576
+        this.removeSegPopover(); // removes the "more.." events popover
6577
+        _super.prototype.executeEventUnrender.call(this);
6578
     };
6579
-    return BasicView;
6580
-}(View_1.default));
6581
-exports.default = BasicView;
6582
-BasicView.prototype.dateProfileGeneratorClass = BasicViewDateProfileGenerator_1.default;
6583
-BasicView.prototype.dayGridClass = DayGrid_1.default;
6584
-// customize the rendering behavior of BasicView's dayGrid
6585
-function makeDayGridSubclass(SuperClass) {
6586
-    return /** @class */ (function (_super) {
6587
-        tslib_1.__extends(SubClass, _super);
6588
-        function SubClass() {
6589
-            var _this = _super !== null && _super.apply(this, arguments) || this;
6590
-            _this.colWeekNumbersVisible = false; // display week numbers along the side?
6591
-            return _this;
6592
-        }
6593
-        // Generates the HTML that will go before the day-of week header cells
6594
-        SubClass.prototype.renderHeadIntroHtml = function () {
6595
-            var view = this.view;
6596
-            if (this.colWeekNumbersVisible) {
6597
-                return '' +
6598
-                    '<th class="fc-week-number ' + view.calendar.theme.getClass('widgetHeader') + '" ' + view.weekNumberStyleAttr() + '>' +
6599
-                    '<span>' + // needed for matchCellWidths
6600
-                    util_1.htmlEscape(this.opt('weekNumberTitle')) +
6601
-                    '</span>' +
6602
-                    '</th>';
6603
-            }
6604
-            return '';
6605
-        };
6606
-        // Generates the HTML that will go before content-skeleton cells that display the day/week numbers
6607
-        SubClass.prototype.renderNumberIntroHtml = function (row) {
6608
-            var view = this.view;
6609
-            var weekStart = this.getCellDate(row, 0);
6610
-            if (this.colWeekNumbersVisible) {
6611
-                return '' +
6612
-                    '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' +
6613
-                    view.buildGotoAnchorHtml(// aside from link, important for matchCellWidths
6614
-                    { date: weekStart, type: 'week', forceOff: this.colCnt === 1 }, weekStart.format('w') // inner HTML
6615
-                    ) +
6616
-                    '</td>';
6617
-            }
6618
-            return '';
6619
-        };
6620
-        // Generates the HTML that goes before the day bg cells for each day-row
6621
-        SubClass.prototype.renderBgIntroHtml = function () {
6622
-            var view = this.view;
6623
-            if (this.colWeekNumbersVisible) {
6624
-                return '<td class="fc-week-number ' + view.calendar.theme.getClass('widgetContent') + '" ' +
6625
-                    view.weekNumberStyleAttr() + '></td>';
6626
-            }
6627
-            return '';
6628
-        };
6629
-        // Generates the HTML that goes before every other type of row generated by DayGrid.
6630
-        // Affects helper-skeleton and highlight-skeleton rows.
6631
-        SubClass.prototype.renderIntroHtml = function () {
6632
-            var view = this.view;
6633
-            if (this.colWeekNumbersVisible) {
6634
-                return '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '></td>';
6635
-            }
6636
-            return '';
6637
-        };
6638
-        SubClass.prototype.getIsNumbersVisible = function () {
6639
-            return DayGrid_1.default.prototype.getIsNumbersVisible.apply(this, arguments) || this.colWeekNumbersVisible;
6640
-        };
6641
-        return SubClass;
6642
-    }(SuperClass));
6643
-}
6644
-
6645
-
6646
-/***/ }),
6647
-/* 63 */,
6648
-/* 64 */,
6649
-/* 65 */,
6650
-/* 66 */,
6651
-/* 67 */,
6652
-/* 68 */,
6653
-/* 69 */,
6654
-/* 70 */,
6655
-/* 71 */,
6656
-/* 72 */,
6657
-/* 73 */,
6658
-/* 74 */,
6659
-/* 75 */,
6660
-/* 76 */,
6661
-/* 77 */,
6662
-/* 78 */,
6663
-/* 79 */,
6664
-/* 80 */,
6665
-/* 81 */,
6666
-/* 82 */,
6667
-/* 83 */,
6668
-/* 84 */,
6669
-/* 85 */,
6670
-/* 86 */,
6671
-/* 87 */,
6672
-/* 88 */,
6673
-/* 89 */,
6674
-/* 90 */,
6675
-/* 91 */,
6676
-/* 92 */,
6677
-/* 93 */,
6678
-/* 94 */,
6679
-/* 95 */,
6680
-/* 96 */,
6681
-/* 97 */,
6682
-/* 98 */,
6683
-/* 99 */,
6684
-/* 100 */,
6685
-/* 101 */,
6686
-/* 102 */,
6687
-/* 103 */,
6688
-/* 104 */,
6689
-/* 105 */,
6690
-/* 106 */,
6691
-/* 107 */,
6692
-/* 108 */,
6693
-/* 109 */,
6694
-/* 110 */,
6695
-/* 111 */,
6696
-/* 112 */,
6697
-/* 113 */,
6698
-/* 114 */,
6699
-/* 115 */,
6700
-/* 116 */,
6701
-/* 117 */,
6702
-/* 118 */,
6703
-/* 119 */,
6704
-/* 120 */,
6705
-/* 121 */,
6706
-/* 122 */,
6707
-/* 123 */,
6708
-/* 124 */,
6709
-/* 125 */,
6710
-/* 126 */,
6711
-/* 127 */,
6712
-/* 128 */,
6713
-/* 129 */,
6714
-/* 130 */,
6715
-/* 131 */,
6716
-/* 132 */,
6717
-/* 133 */,
6718
-/* 134 */,
6719
-/* 135 */,
6720
-/* 136 */,
6721
-/* 137 */,
6722
-/* 138 */,
6723
-/* 139 */,
6724
-/* 140 */,
6725
-/* 141 */,
6726
-/* 142 */,
6727
-/* 143 */,
6728
-/* 144 */,
6729
-/* 145 */,
6730
-/* 146 */,
6731
-/* 147 */,
6732
-/* 148 */,
6733
-/* 149 */,
6734
-/* 150 */,
6735
-/* 151 */,
6736
-/* 152 */,
6737
-/* 153 */,
6738
-/* 154 */,
6739
-/* 155 */,
6740
-/* 156 */,
6741
-/* 157 */,
6742
-/* 158 */,
6743
-/* 159 */,
6744
-/* 160 */,
6745
-/* 161 */,
6746
-/* 162 */,
6747
-/* 163 */,
6748
-/* 164 */,
6749
-/* 165 */,
6750
-/* 166 */,
6751
-/* 167 */,
6752
-/* 168 */,
6753
-/* 169 */,
6754
-/* 170 */,
6755
-/* 171 */,
6756
-/* 172 */,
6757
-/* 173 */,
6758
-/* 174 */,
6759
-/* 175 */,
6760
-/* 176 */,
6761
-/* 177 */,
6762
-/* 178 */,
6763
-/* 179 */,
6764
-/* 180 */,
6765
-/* 181 */,
6766
-/* 182 */,
6767
-/* 183 */,
6768
-/* 184 */,
6769
-/* 185 */,
6770
-/* 186 */,
6771
-/* 187 */,
6772
-/* 188 */,
6773
-/* 189 */,
6774
-/* 190 */,
6775
-/* 191 */,
6776
-/* 192 */,
6777
-/* 193 */,
6778
-/* 194 */,
6779
-/* 195 */,
6780
-/* 196 */,
6781
-/* 197 */,
6782
-/* 198 */,
6783
-/* 199 */,
6784
-/* 200 */,
6785
-/* 201 */,
6786
-/* 202 */,
6787
-/* 203 */,
6788
-/* 204 */,
6789
-/* 205 */,
6790
-/* 206 */,
6791
-/* 207 */
6792
-/***/ (function(module, exports, __webpack_require__) {
6793
-
6794
-Object.defineProperty(exports, "__esModule", { value: true });
6795
-var UnzonedRange_1 = __webpack_require__(5);
6796
-var ComponentFootprint_1 = __webpack_require__(12);
6797
-var EventDefParser_1 = __webpack_require__(49);
6798
-var EventSource_1 = __webpack_require__(6);
6799
-var util_1 = __webpack_require__(35);
6800
-var Constraints = /** @class */ (function () {
6801
-    function Constraints(eventManager, _calendar) {
6802
-        this.eventManager = eventManager;
6803
-        this._calendar = _calendar;
6804
-    }
6805
-    Constraints.prototype.opt = function (name) {
6806
-        return this._calendar.opt(name);
6807
-    };
6808
-    /*
6809
-    determines if eventInstanceGroup is allowed,
6810
-    in relation to other EVENTS and business hours.
6811
-    */
6812
-    Constraints.prototype.isEventInstanceGroupAllowed = function (eventInstanceGroup) {
6813
-        var eventDef = eventInstanceGroup.getEventDef();
6814
-        var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
6815
-        var i;
6816
-        var peerEventInstances = this.getPeerEventInstances(eventDef);
6817
-        var peerEventRanges = peerEventInstances.map(util_1.eventInstanceToEventRange);
6818
-        var peerEventFootprints = this.eventRangesToEventFootprints(peerEventRanges);
6819
-        var constraintVal = eventDef.getConstraint();
6820
-        var overlapVal = eventDef.getOverlap();
6821
-        var eventAllowFunc = this.opt('eventAllow');
6822
-        for (i = 0; i < eventFootprints.length; i++) {
6823
-            if (!this.isFootprintAllowed(eventFootprints[i].componentFootprint, peerEventFootprints, constraintVal, overlapVal, eventFootprints[i].eventInstance)) {
6824
-                return false;
6825
-            }
6826
-        }
6827
-        if (eventAllowFunc) {
6828
-            for (i = 0; i < eventFootprints.length; i++) {
6829
-                if (eventAllowFunc(eventFootprints[i].componentFootprint.toLegacy(this._calendar), eventFootprints[i].getEventLegacy()) === false) {
6830
-                    return false;
6831
-                }
6832
-            }
6833
-        }
6834
-        return true;
6835
-    };
6836
-    Constraints.prototype.getPeerEventInstances = function (eventDef) {
6837
-        return this.eventManager.getEventInstancesWithoutId(eventDef.id);
6838
-    };
6839
-    Constraints.prototype.isSelectionFootprintAllowed = function (componentFootprint) {
6840
-        var peerEventInstances = this.eventManager.getEventInstances();
6841
-        var peerEventRanges = peerEventInstances.map(util_1.eventInstanceToEventRange);
6842
-        var peerEventFootprints = this.eventRangesToEventFootprints(peerEventRanges);
6843
-        var selectAllowFunc;
6844
-        if (this.isFootprintAllowed(componentFootprint, peerEventFootprints, this.opt('selectConstraint'), this.opt('selectOverlap'))) {
6845
-            selectAllowFunc = this.opt('selectAllow');
6846
-            if (selectAllowFunc) {
6847
-                return selectAllowFunc(componentFootprint.toLegacy(this._calendar)) !== false;
6848
-            }
6849
-            else {
6850
-                return true;
6851
-            }
6852
-        }
6853
-        return false;
6854
-    };
6855
-    Constraints.prototype.isFootprintAllowed = function (componentFootprint, peerEventFootprints, constraintVal, overlapVal, subjectEventInstance // optional
6856
-    ) {
6857
-        var constraintFootprints; // ComponentFootprint[]
6858
-        var overlapEventFootprints; // EventFootprint[]
6859
-        if (constraintVal != null) {
6860
-            constraintFootprints = this.constraintValToFootprints(constraintVal, componentFootprint.isAllDay);
6861
-            if (!this.isFootprintWithinConstraints(componentFootprint, constraintFootprints)) {
6862
-                return false;
6863
-            }
6864
-        }
6865
-        overlapEventFootprints = this.collectOverlapEventFootprints(peerEventFootprints, componentFootprint);
6866
-        if (overlapVal === false) {
6867
-            if (overlapEventFootprints.length) {
6868
-                return false;
6869
-            }
6870
-        }
6871
-        else if (typeof overlapVal === 'function') {
6872
-            if (!isOverlapsAllowedByFunc(overlapEventFootprints, overlapVal, subjectEventInstance)) {
6873
-                return false;
6874
-            }
6875
-        }
6876
-        if (subjectEventInstance) {
6877
-            if (!isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInstance)) {
6878
-                return false;
6879
-            }
6880
-        }
6881
-        return true;
6882
-    };
6883
-    // Constraint
6884
-    // ------------------------------------------------------------------------------------------------
6885
-    Constraints.prototype.isFootprintWithinConstraints = function (componentFootprint, constraintFootprints) {
6886
-        var i;
6887
-        for (i = 0; i < constraintFootprints.length; i++) {
6888
-            if (this.footprintContainsFootprint(constraintFootprints[i], componentFootprint)) {
6889
-                return true;
6890
-            }
6891
-        }
6892
-        return false;
6893
-    };
6894
-    Constraints.prototype.constraintValToFootprints = function (constraintVal, isAllDay) {
6895
-        var eventInstances;
6896
-        if (constraintVal === 'businessHours') {
6897
-            return this.buildCurrentBusinessFootprints(isAllDay);
6898
-        }
6899
-        else if (typeof constraintVal === 'object') {
6900
-            eventInstances = this.parseEventDefToInstances(constraintVal); // handles recurring events
6901
-            if (!eventInstances) {
6902
-                return this.parseFootprints(constraintVal);
6903
-            }
6904
-            else {
6905
-                return this.eventInstancesToFootprints(eventInstances);
6906
-            }
6907
-        }
6908
-        else if (constraintVal != null) {
6909
-            eventInstances = this.eventManager.getEventInstancesWithId(constraintVal);
6910
-            return this.eventInstancesToFootprints(eventInstances);
6911
-        }
6912
-    };
6913
-    // returns ComponentFootprint[]
6914
-    // uses current view's range
6915
-    Constraints.prototype.buildCurrentBusinessFootprints = function (isAllDay) {
6916
-        var view = this._calendar.view;
6917
-        var businessHourGenerator = view.get('businessHourGenerator');
6918
-        var unzonedRange = view.dateProfile.activeUnzonedRange;
6919
-        var eventInstanceGroup = businessHourGenerator.buildEventInstanceGroup(isAllDay, unzonedRange);
6920
-        if (eventInstanceGroup) {
6921
-            return this.eventInstancesToFootprints(eventInstanceGroup.eventInstances);
6922
-        }
6923
-        else {
6924
-            return [];
6925
-        }
6926
-    };
6927
-    // conversion util
6928
-    Constraints.prototype.eventInstancesToFootprints = function (eventInstances) {
6929
-        var eventRanges = eventInstances.map(util_1.eventInstanceToEventRange);
6930
-        var eventFootprints = this.eventRangesToEventFootprints(eventRanges);
6931
-        return eventFootprints.map(util_1.eventFootprintToComponentFootprint);
6932
-    };
6933
-    // Overlap
6934
-    // ------------------------------------------------------------------------------------------------
6935
-    Constraints.prototype.collectOverlapEventFootprints = function (peerEventFootprints, targetFootprint) {
6936
-        var overlapEventFootprints = [];
6937
-        var i;
6938
-        for (i = 0; i < peerEventFootprints.length; i++) {
6939
-            if (this.footprintsIntersect(targetFootprint, peerEventFootprints[i].componentFootprint)) {
6940
-                overlapEventFootprints.push(peerEventFootprints[i]);
6941
-            }
6942
-        }
6943
-        return overlapEventFootprints;
6944
-    };
6945
-    // Conversion: eventDefs -> eventInstances -> eventRanges -> eventFootprints -> componentFootprints
6946
-    // ------------------------------------------------------------------------------------------------
6947
-    // NOTE: this might seem like repetitive code with the Grid class, however, this code is related to
6948
-    // constraints whereas the Grid code is related to rendering. Each approach might want to convert
6949
-    // eventRanges -> eventFootprints in a different way. Regardless, there are opportunities to make
6950
-    // this more DRY.
6951
-    /*
6952
-    Returns false on invalid input.
6953
-    */
6954
-    Constraints.prototype.parseEventDefToInstances = function (eventInput) {
6955
-        var eventManager = this.eventManager;
6956
-        var eventDef = EventDefParser_1.default.parse(eventInput, new EventSource_1.default(this._calendar));
6957
-        if (!eventDef) {
6958
-            return false;
6959
-        }
6960
-        return eventDef.buildInstances(eventManager.currentPeriod.unzonedRange);
6961
-    };
6962
-    Constraints.prototype.eventRangesToEventFootprints = function (eventRanges) {
6963
-        var i;
6964
-        var eventFootprints = [];
6965
-        for (i = 0; i < eventRanges.length; i++) {
6966
-            eventFootprints.push.apply(// footprints
6967
-            eventFootprints, this.eventRangeToEventFootprints(eventRanges[i]));
6968
-        }
6969
-        return eventFootprints;
6970
-    };
6971
-    Constraints.prototype.eventRangeToEventFootprints = function (eventRange) {
6972
-        return [util_1.eventRangeToEventFootprint(eventRange)];
6973
-    };
6974
-    /*
6975
-    Parses footprints directly.
6976
-    Very similar to EventDateProfile::parse :(
6977
-    */
6978
-    Constraints.prototype.parseFootprints = function (rawInput) {
6979
-        var start;
6980
-        var end;
6981
-        if (rawInput.start) {
6982
-            start = this._calendar.moment(rawInput.start);
6983
-            if (!start.isValid()) {
6984
-                start = null;
6985
-            }
6986
-        }
6987
-        if (rawInput.end) {
6988
-            end = this._calendar.moment(rawInput.end);
6989
-            if (!end.isValid()) {
6990
-                end = null;
6991
-            }
6992
-        }
6993
-        return [
6994
-            new ComponentFootprint_1.default(new UnzonedRange_1.default(start, end), (start && !start.hasTime()) || (end && !end.hasTime()) // isAllDay
6995
-            )
6996
-        ];
6997
-    };
6998
-    // Footprint Utils
6999
-    // ----------------------------------------------------------------------------------------
7000
-    Constraints.prototype.footprintContainsFootprint = function (outerFootprint, innerFootprint) {
7001
-        return outerFootprint.unzonedRange.containsRange(innerFootprint.unzonedRange);
7002
-    };
7003
-    Constraints.prototype.footprintsIntersect = function (footprint0, footprint1) {
7004
-        return footprint0.unzonedRange.intersectsWith(footprint1.unzonedRange);
7005
-    };
7006
-    return Constraints;
7007
-}());
7008
-exports.default = Constraints;
7009
-// optional subjectEventInstance
7010
-function isOverlapsAllowedByFunc(overlapEventFootprints, overlapFunc, subjectEventInstance) {
7011
-    var i;
7012
-    for (i = 0; i < overlapEventFootprints.length; i++) {
7013
-        if (!overlapFunc(overlapEventFootprints[i].eventInstance.toLegacy(), subjectEventInstance ? subjectEventInstance.toLegacy() : null)) {
7014
-            return false;
7015
-        }
7016
-    }
7017
-    return true;
7018
-}
7019
-function isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInstance) {
7020
-    var subjectLegacyInstance = subjectEventInstance.toLegacy();
7021
-    var i;
7022
-    var overlapEventInstance;
7023
-    var overlapEventDef;
7024
-    var overlapVal;
7025
-    for (i = 0; i < overlapEventFootprints.length; i++) {
7026
-        overlapEventInstance = overlapEventFootprints[i].eventInstance;
7027
-        overlapEventDef = overlapEventInstance.def;
7028
-        // don't need to pass in calendar, because don't want to consider global eventOverlap property,
7029
-        // because we already considered that earlier in the process.
7030
-        overlapVal = overlapEventDef.getOverlap();
7031
-        if (overlapVal === false) {
7032
-            return false;
7033
-        }
7034
-        else if (typeof overlapVal === 'function') {
7035
-            if (!overlapVal(overlapEventInstance.toLegacy(), subjectLegacyInstance)) {
7036
-                return false;
7037
-            }
7038
-        }
7039
-    }
7040
-    return true;
7041
-}
7042
-
7043
-
7044
-/***/ }),
7045
-/* 208 */
7046
-/***/ (function(module, exports, __webpack_require__) {
7047
-
7048
-/*
7049
-USAGE:
7050
-  import { default as ParsableModelMixin, ParsableModelInterface } from './ParsableModelMixin'
7051
-in class:
7052
-  applyProps: ParsableModelInterface['applyProps']
7053
-  applyManualStandardProps: ParsableModelInterface['applyManualStandardProps']
7054
-  applyMiscProps: ParsableModelInterface['applyMiscProps']
7055
-  isStandardProp: ParsableModelInterface['isStandardProp']
7056
-  static defineStandardProps = ParsableModelMixin.defineStandardProps
7057
-  static copyVerbatimStandardProps = ParsableModelMixin.copyVerbatimStandardProps
7058
-after class:
7059
-  ParsableModelMixin.mixInto(TheClass)
7060
-*/
7061
-Object.defineProperty(exports, "__esModule", { value: true });
7062
-var tslib_1 = __webpack_require__(2);
7063
-var util_1 = __webpack_require__(4);
7064
-var Mixin_1 = __webpack_require__(14);
7065
-var ParsableModelMixin = /** @class */ (function (_super) {
7066
-    tslib_1.__extends(ParsableModelMixin, _super);
7067
-    function ParsableModelMixin() {
7068
-        return _super !== null && _super.apply(this, arguments) || this;
7069
-    }
7070
-    ParsableModelMixin.defineStandardProps = function (propDefs) {
7071
-        var proto = this.prototype;
7072
-        if (!proto.hasOwnProperty('standardPropMap')) {
7073
-            proto.standardPropMap = Object.create(proto.standardPropMap);
7074
-        }
7075
-        util_1.copyOwnProps(propDefs, proto.standardPropMap);
7076
-    };
7077
-    ParsableModelMixin.copyVerbatimStandardProps = function (src, dest) {
7078
-        var map = this.prototype.standardPropMap;
7079
-        var propName;
7080
-        for (propName in map) {
7081
-            if (src[propName] != null && // in the src object?
7082
-                map[propName] === true // false means "copy verbatim"
7083
-            ) {
7084
-                dest[propName] = src[propName];
7085
-            }
7086
-        }
7087
-    };
7088
-    /*
7089
-    Returns true/false for success.
7090
-    Meant to be only called ONCE, at object creation.
7091
-    */
7092
-    ParsableModelMixin.prototype.applyProps = function (rawProps) {
7093
-        var standardPropMap = this.standardPropMap;
7094
-        var manualProps = {};
7095
-        var miscProps = {};
7096
-        var propName;
7097
-        for (propName in rawProps) {
7098
-            if (standardPropMap[propName] === true) {
7099
-                this[propName] = rawProps[propName];
7100
-            }
7101
-            else if (standardPropMap[propName] === false) {
7102
-                manualProps[propName] = rawProps[propName];
7103
-            }
7104
-            else {
7105
-                miscProps[propName] = rawProps[propName];
7106
-            }
7107
-        }
7108
-        this.applyMiscProps(miscProps);
7109
-        return this.applyManualStandardProps(manualProps);
7110
-    };
7111
-    /*
7112
-    If subclasses override, they must call this supermethod and return the boolean response.
7113
-    Meant to be only called ONCE, at object creation.
7114
-    */
7115
-    ParsableModelMixin.prototype.applyManualStandardProps = function (rawProps) {
7116
-        return true;
7117
-    };
7118
-    /*
7119
-    Can be called even after initial object creation.
7120
-    */
7121
-    ParsableModelMixin.prototype.applyMiscProps = function (rawProps) {
7122
-        // subclasses can implement
7123
-    };
7124
-    /*
7125
-    TODO: why is this a method when defineStandardProps is static
7126
-    */
7127
-    ParsableModelMixin.prototype.isStandardProp = function (propName) {
7128
-        return propName in this.standardPropMap;
7129
-    };
7130
-    return ParsableModelMixin;
7131
-}(Mixin_1.default));
7132
-exports.default = ParsableModelMixin;
7133
-ParsableModelMixin.prototype.standardPropMap = {}; // will be cloned by defineStandardProps
7134
-
7135
-
7136
-/***/ }),
7137
-/* 209 */
7138
-/***/ (function(module, exports) {
7139
-
7140
-Object.defineProperty(exports, "__esModule", { value: true });
7141
-var EventInstance = /** @class */ (function () {
7142
-    function EventInstance(def, dateProfile) {
7143
-        this.def = def;
7144
-        this.dateProfile = dateProfile;
7145
-    }
7146
-    EventInstance.prototype.toLegacy = function () {
7147
-        var dateProfile = this.dateProfile;
7148
-        var obj = this.def.toLegacy();
7149
-        obj.start = dateProfile.start.clone();
7150
-        obj.end = dateProfile.end ? dateProfile.end.clone() : null;
7151
-        return obj;
7152
-    };
7153
-    return EventInstance;
7154
-}());
7155
-exports.default = EventInstance;
7156
-
7157
-
7158
-/***/ }),
7159
-/* 210 */
7160
-/***/ (function(module, exports, __webpack_require__) {
7161
-
7162
-Object.defineProperty(exports, "__esModule", { value: true });
7163
-var tslib_1 = __webpack_require__(2);
7164
-var $ = __webpack_require__(3);
7165
-var moment = __webpack_require__(0);
7166
-var EventDef_1 = __webpack_require__(34);
7167
-var EventInstance_1 = __webpack_require__(209);
7168
-var EventDateProfile_1 = __webpack_require__(17);
7169
-var RecurringEventDef = /** @class */ (function (_super) {
7170
-    tslib_1.__extends(RecurringEventDef, _super);
7171
-    function RecurringEventDef() {
7172
-        return _super !== null && _super.apply(this, arguments) || this;
7173
-    }
7174
-    RecurringEventDef.prototype.isAllDay = function () {
7175
-        return !this.startTime && !this.endTime;
7176
-    };
7177
-    RecurringEventDef.prototype.buildInstances = function (unzonedRange) {
7178
-        var calendar = this.source.calendar;
7179
-        var unzonedDate = unzonedRange.getStart();
7180
-        var unzonedEnd = unzonedRange.getEnd();
7181
-        var zonedDayStart;
7182
-        var instanceStart;
7183
-        var instanceEnd;
7184
-        var instances = [];
7185
-        while (unzonedDate.isBefore(unzonedEnd)) {
7186
-            // if everyday, or this particular day-of-week
7187
-            if (!this.dowHash || this.dowHash[unzonedDate.day()]) {
7188
-                zonedDayStart = calendar.applyTimezone(unzonedDate);
7189
-                instanceStart = zonedDayStart.clone();
7190
-                instanceEnd = null;
7191
-                if (this.startTime) {
7192
-                    instanceStart.time(this.startTime);
7193
-                }
7194
-                else {
7195
-                    instanceStart.stripTime();
7196
-                }
7197
-                if (this.endTime) {
7198
-                    instanceEnd = zonedDayStart.clone().time(this.endTime);
7199
-                }
7200
-                instances.push(new EventInstance_1.default(this, // definition
7201
-                new EventDateProfile_1.default(instanceStart, instanceEnd, calendar)));
7202
-            }
7203
-            unzonedDate.add(1, 'days');
7204
-        }
7205
-        return instances;
7206
-    };
7207
-    RecurringEventDef.prototype.setDow = function (dowNumbers) {
7208
-        if (!this.dowHash) {
7209
-            this.dowHash = {};
7210
-        }
7211
-        for (var i = 0; i < dowNumbers.length; i++) {
7212
-            this.dowHash[dowNumbers[i]] = true;
7213
-        }
7214
-    };
7215
-    RecurringEventDef.prototype.clone = function () {
7216
-        var def = _super.prototype.clone.call(this);
7217
-        if (def.startTime) {
7218
-            def.startTime = moment.duration(this.startTime);
7219
-        }
7220
-        if (def.endTime) {
7221
-            def.endTime = moment.duration(this.endTime);
7222
-        }
7223
-        if (this.dowHash) {
7224
-            def.dowHash = $.extend({}, this.dowHash);
7225
-        }
7226
-        return def;
7227
-    };
7228
-    return RecurringEventDef;
7229
-}(EventDef_1.default));
7230
-exports.default = RecurringEventDef;
7231
-/*
7232
-HACK to work with TypeScript mixins
7233
-NOTE: if super-method fails, should still attempt to apply
7234
-*/
7235
-RecurringEventDef.prototype.applyProps = function (rawProps) {
7236
-    var superSuccess = EventDef_1.default.prototype.applyProps.call(this, rawProps);
7237
-    if (rawProps.start) {
7238
-        this.startTime = moment.duration(rawProps.start);
7239
-    }
7240
-    if (rawProps.end) {
7241
-        this.endTime = moment.duration(rawProps.end);
7242
-    }
7243
-    if (rawProps.dow) {
7244
-        this.setDow(rawProps.dow);
7245
-    }
7246
-    return superSuccess;
7247
-};
7248
-// Parsing
7249
-// ---------------------------------------------------------------------------------------------------------------------
7250
-RecurringEventDef.defineStandardProps({
7251
-    start: false,
7252
-    end: false,
7253
-    dow: false
7254
-});
7255
-
7256
-
7257
-/***/ }),
7258
-/* 211 */
7259
-/***/ (function(module, exports) {
7260
-
7261
-Object.defineProperty(exports, "__esModule", { value: true });
7262
-var EventRange = /** @class */ (function () {
7263
-    function EventRange(unzonedRange, eventDef, eventInstance) {
7264
-        this.unzonedRange = unzonedRange;
7265
-        this.eventDef = eventDef;
7266
-        if (eventInstance) {
7267
-            this.eventInstance = eventInstance;
7268
-        }
7269
-    }
7270
-    return EventRange;
7271
-}());
7272
-exports.default = EventRange;
7273
-
7274
-
7275
-/***/ }),
7276
-/* 212 */
7277
-/***/ (function(module, exports, __webpack_require__) {
7278
-
7279
-Object.defineProperty(exports, "__esModule", { value: true });
7280
-var $ = __webpack_require__(3);
7281
-var util_1 = __webpack_require__(35);
7282
-var EventInstanceGroup_1 = __webpack_require__(18);
7283
-var RecurringEventDef_1 = __webpack_require__(210);
7284
-var EventSource_1 = __webpack_require__(6);
7285
-var BUSINESS_HOUR_EVENT_DEFAULTS = {
7286
-    start: '09:00',
7287
-    end: '17:00',
7288
-    dow: [1, 2, 3, 4, 5],
7289
-    rendering: 'inverse-background'
7290
-    // classNames are defined in businessHoursSegClasses
7291
-};
7292
-var BusinessHourGenerator = /** @class */ (function () {
7293
-    function BusinessHourGenerator(rawComplexDef, calendar) {
7294
-        this.rawComplexDef = rawComplexDef;
7295
-        this.calendar = calendar;
7296
-    }
7297
-    BusinessHourGenerator.prototype.buildEventInstanceGroup = function (isAllDay, unzonedRange) {
7298
-        var eventDefs = this.buildEventDefs(isAllDay);
7299
-        var eventInstanceGroup;
7300
-        if (eventDefs.length) {
7301
-            eventInstanceGroup = new EventInstanceGroup_1.default(util_1.eventDefsToEventInstances(eventDefs, unzonedRange));
7302
-            // so that inverse-background rendering can happen even when no eventRanges in view
7303
-            eventInstanceGroup.explicitEventDef = eventDefs[0];
7304
-            return eventInstanceGroup;
7305
-        }
7306
-    };
7307
-    BusinessHourGenerator.prototype.buildEventDefs = function (isAllDay) {
7308
-        var rawComplexDef = this.rawComplexDef;
7309
-        var rawDefs = [];
7310
-        var requireDow = false;
7311
-        var i;
7312
-        var defs = [];
7313
-        if (rawComplexDef === true) {
7314
-            rawDefs = [{}]; // will get BUSINESS_HOUR_EVENT_DEFAULTS verbatim
7315
-        }
7316
-        else if ($.isPlainObject(rawComplexDef)) {
7317
-            rawDefs = [rawComplexDef];
7318
-        }
7319
-        else if ($.isArray(rawComplexDef)) {
7320
-            rawDefs = rawComplexDef;
7321
-            requireDow = true; // every sub-definition NEEDS a day-of-week
7322
-        }
7323
-        for (i = 0; i < rawDefs.length; i++) {
7324
-            if (!requireDow || rawDefs[i].dow) {
7325
-                defs.push(this.buildEventDef(isAllDay, rawDefs[i]));
7326
-            }
7327
-        }
7328
-        return defs;
7329
-    };
7330
-    BusinessHourGenerator.prototype.buildEventDef = function (isAllDay, rawDef) {
7331
-        var fullRawDef = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, rawDef);
7332
-        if (isAllDay) {
7333
-            fullRawDef.start = null;
7334
-            fullRawDef.end = null;
7335
-        }
7336
-        return RecurringEventDef_1.default.parse(fullRawDef, new EventSource_1.default(this.calendar) // dummy source
7337
-        );
7338
-    };
7339
-    return BusinessHourGenerator;
7340
-}());
7341
-exports.default = BusinessHourGenerator;
7342
-
7343
-
7344
-/***/ }),
7345
-/* 213 */
7346
-/***/ (function(module, exports, __webpack_require__) {
7347
-
7348
-Object.defineProperty(exports, "__esModule", { value: true });
7349
-var tslib_1 = __webpack_require__(2);
7350
-var Theme_1 = __webpack_require__(19);
7351
-var StandardTheme = /** @class */ (function (_super) {
7352
-    tslib_1.__extends(StandardTheme, _super);
7353
-    function StandardTheme() {
7354
-        return _super !== null && _super.apply(this, arguments) || this;
7355
-    }
7356
-    return StandardTheme;
7357
-}(Theme_1.default));
7358
-exports.default = StandardTheme;
7359
-StandardTheme.prototype.classes = {
7360
-    widget: 'fc-unthemed',
7361
-    widgetHeader: 'fc-widget-header',
7362
-    widgetContent: 'fc-widget-content',
7363
-    buttonGroup: 'fc-button-group',
7364
-    button: 'fc-button',
7365
-    cornerLeft: 'fc-corner-left',
7366
-    cornerRight: 'fc-corner-right',
7367
-    stateDefault: 'fc-state-default',
7368
-    stateActive: 'fc-state-active',
7369
-    stateDisabled: 'fc-state-disabled',
7370
-    stateHover: 'fc-state-hover',
7371
-    stateDown: 'fc-state-down',
7372
-    popoverHeader: 'fc-widget-header',
7373
-    popoverContent: 'fc-widget-content',
7374
-    // day grid
7375
-    headerRow: 'fc-widget-header',
7376
-    dayRow: 'fc-widget-content',
7377
-    // list view
7378
-    listView: 'fc-widget-content'
7379
-};
7380
-StandardTheme.prototype.baseIconClass = 'fc-icon';
7381
-StandardTheme.prototype.iconClasses = {
7382
-    close: 'fc-icon-x',
7383
-    prev: 'fc-icon-left-single-arrow',
7384
-    next: 'fc-icon-right-single-arrow',
7385
-    prevYear: 'fc-icon-left-double-arrow',
7386
-    nextYear: 'fc-icon-right-double-arrow'
7387
-};
7388
-StandardTheme.prototype.iconOverrideOption = 'buttonIcons';
7389
-StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon';
7390
-StandardTheme.prototype.iconOverridePrefix = 'fc-icon-';
7391
-
7392
-
7393
-/***/ }),
7394
-/* 214 */
7395
-/***/ (function(module, exports, __webpack_require__) {
7396
-
7397
-Object.defineProperty(exports, "__esModule", { value: true });
7398
-var tslib_1 = __webpack_require__(2);
7399
-var Theme_1 = __webpack_require__(19);
7400
-var JqueryUiTheme = /** @class */ (function (_super) {
7401
-    tslib_1.__extends(JqueryUiTheme, _super);
7402
-    function JqueryUiTheme() {
7403
-        return _super !== null && _super.apply(this, arguments) || this;
7404
-    }
7405
-    return JqueryUiTheme;
7406
-}(Theme_1.default));
7407
-exports.default = JqueryUiTheme;
7408
-JqueryUiTheme.prototype.classes = {
7409
-    widget: 'ui-widget',
7410
-    widgetHeader: 'ui-widget-header',
7411
-    widgetContent: 'ui-widget-content',
7412
-    buttonGroup: 'fc-button-group',
7413
-    button: 'ui-button',
7414
-    cornerLeft: 'ui-corner-left',
7415
-    cornerRight: 'ui-corner-right',
7416
-    stateDefault: 'ui-state-default',
7417
-    stateActive: 'ui-state-active',
7418
-    stateDisabled: 'ui-state-disabled',
7419
-    stateHover: 'ui-state-hover',
7420
-    stateDown: 'ui-state-down',
7421
-    today: 'ui-state-highlight',
7422
-    popoverHeader: 'ui-widget-header',
7423
-    popoverContent: 'ui-widget-content',
7424
-    // day grid
7425
-    headerRow: 'ui-widget-header',
7426
-    dayRow: 'ui-widget-content',
7427
-    // list view
7428
-    listView: 'ui-widget-content'
7429
-};
7430
-JqueryUiTheme.prototype.baseIconClass = 'ui-icon';
7431
-JqueryUiTheme.prototype.iconClasses = {
7432
-    close: 'ui-icon-closethick',
7433
-    prev: 'ui-icon-circle-triangle-w',
7434
-    next: 'ui-icon-circle-triangle-e',
7435
-    prevYear: 'ui-icon-seek-prev',
7436
-    nextYear: 'ui-icon-seek-next'
7437
-};
7438
-JqueryUiTheme.prototype.iconOverrideOption = 'themeButtonIcons';
7439
-JqueryUiTheme.prototype.iconOverrideCustomButtonOption = 'themeIcon';
7440
-JqueryUiTheme.prototype.iconOverridePrefix = 'ui-icon-';
7441
-
7442
-
7443
-/***/ }),
7444
-/* 215 */
7445
-/***/ (function(module, exports, __webpack_require__) {
7446
-
7447
-Object.defineProperty(exports, "__esModule", { value: true });
7448
-var tslib_1 = __webpack_require__(2);
7449
-var $ = __webpack_require__(3);
7450
-var Promise_1 = __webpack_require__(20);
7451
-var EventSource_1 = __webpack_require__(6);
7452
-var FuncEventSource = /** @class */ (function (_super) {
7453
-    tslib_1.__extends(FuncEventSource, _super);
7454
-    function FuncEventSource() {
7455
-        return _super !== null && _super.apply(this, arguments) || this;
7456
-    }
7457
-    FuncEventSource.parse = function (rawInput, calendar) {
7458
-        var rawProps;
7459
-        // normalize raw input
7460
-        if ($.isFunction(rawInput.events)) {
7461
-            rawProps = rawInput;
7462
-        }
7463
-        else if ($.isFunction(rawInput)) {
7464
-            rawProps = { events: rawInput };
7465
-        }
7466
-        if (rawProps) {
7467
-            return EventSource_1.default.parse.call(this, rawProps, calendar);
7468
-        }
7469
-        return false;
7470
-    };
7471
-    FuncEventSource.prototype.fetch = function (start, end, timezone) {
7472
-        var _this = this;
7473
-        this.calendar.pushLoading();
7474
-        return Promise_1.default.construct(function (onResolve) {
7475
-            _this.func.call(_this.calendar, start.clone(), end.clone(), timezone, function (rawEventDefs) {
7476
-                _this.calendar.popLoading();
7477
-                onResolve(_this.parseEventDefs(rawEventDefs));
7478
-            });
7479
-        });
7480
-    };
7481
-    FuncEventSource.prototype.getPrimitive = function () {
7482
-        return this.func;
7483
-    };
7484
-    FuncEventSource.prototype.applyManualStandardProps = function (rawProps) {
7485
-        var superSuccess = _super.prototype.applyManualStandardProps.call(this, rawProps);
7486
-        this.func = rawProps.events;
7487
-        return superSuccess;
7488
-    };
7489
-    return FuncEventSource;
7490
-}(EventSource_1.default));
7491
-exports.default = FuncEventSource;
7492
-FuncEventSource.defineStandardProps({
7493
-    events: false // don't automatically transfer
7494
-});
7495
-
7496
-
7497
-/***/ }),
7498
-/* 216 */
7499
-/***/ (function(module, exports, __webpack_require__) {
7500
-
7501
-Object.defineProperty(exports, "__esModule", { value: true });
7502
-var tslib_1 = __webpack_require__(2);
7503
-var $ = __webpack_require__(3);
7504
-var util_1 = __webpack_require__(4);
7505
-var Promise_1 = __webpack_require__(20);
7506
-var EventSource_1 = __webpack_require__(6);
7507
-var JsonFeedEventSource = /** @class */ (function (_super) {
7508
-    tslib_1.__extends(JsonFeedEventSource, _super);
7509
-    function JsonFeedEventSource() {
7510
-        return _super !== null && _super.apply(this, arguments) || this;
7511
-    }
7512
-    JsonFeedEventSource.parse = function (rawInput, calendar) {
7513
-        var rawProps;
7514
-        // normalize raw input
7515
-        if (typeof rawInput.url === 'string') {
7516
-            rawProps = rawInput;
7517
-        }
7518
-        else if (typeof rawInput === 'string') {
7519
-            rawProps = { url: rawInput };
7520
-        }
7521
-        if (rawProps) {
7522
-            return EventSource_1.default.parse.call(this, rawProps, calendar);
7523
-        }
7524
-        return false;
7525
-    };
7526
-    JsonFeedEventSource.prototype.fetch = function (start, end, timezone) {
7527
-        var _this = this;
7528
-        var ajaxSettings = this.ajaxSettings;
7529
-        var onSuccess = ajaxSettings.success;
7530
-        var onError = ajaxSettings.error;
7531
-        var requestParams = this.buildRequestParams(start, end, timezone);
7532
-        // todo: eventually handle the promise's then,
7533
-        // don't intercept success/error
7534
-        // tho will be a breaking API change
7535
-        this.calendar.pushLoading();
7536
-        return Promise_1.default.construct(function (onResolve, onReject) {
7537
-            $.ajax($.extend({}, // destination
7538
-            JsonFeedEventSource.AJAX_DEFAULTS, ajaxSettings, {
7539
-                url: _this.url,
7540
-                data: requestParams,
7541
-                success: function (rawEventDefs, status, xhr) {
7542
-                    var callbackRes;
7543
-                    _this.calendar.popLoading();
7544
-                    if (rawEventDefs) {
7545
-                        callbackRes = util_1.applyAll(onSuccess, _this, [rawEventDefs, status, xhr]); // redirect `this`
7546
-                        if ($.isArray(callbackRes)) {
7547
-                            rawEventDefs = callbackRes;
7548
-                        }
7549
-                        onResolve(_this.parseEventDefs(rawEventDefs));
7550
-                    }
7551
-                    else {
7552
-                        onReject();
7553
-                    }
7554
-                },
7555
-                error: function (xhr, statusText, errorThrown) {
7556
-                    _this.calendar.popLoading();
7557
-                    util_1.applyAll(onError, _this, [xhr, statusText, errorThrown]); // redirect `this`
7558
-                    onReject();
7559
-                }
7560
-            }));
7561
-        });
7562
-    };
7563
-    JsonFeedEventSource.prototype.buildRequestParams = function (start, end, timezone) {
7564
-        var calendar = this.calendar;
7565
-        var ajaxSettings = this.ajaxSettings;
7566
-        var startParam;
7567
-        var endParam;
7568
-        var timezoneParam;
7569
-        var customRequestParams;
7570
-        var params = {};
7571
-        startParam = this.startParam;
7572
-        if (startParam == null) {
7573
-            startParam = calendar.opt('startParam');
7574
-        }
7575
-        endParam = this.endParam;
7576
-        if (endParam == null) {
7577
-            endParam = calendar.opt('endParam');
7578
-        }
7579
-        timezoneParam = this.timezoneParam;
7580
-        if (timezoneParam == null) {
7581
-            timezoneParam = calendar.opt('timezoneParam');
7582
-        }
7583
-        // retrieve any outbound GET/POST $.ajax data from the options
7584
-        if ($.isFunction(ajaxSettings.data)) {
7585
-            // supplied as a function that returns a key/value object
7586
-            customRequestParams = ajaxSettings.data();
7587
-        }
7588
-        else {
7589
-            // probably supplied as a straight key/value object
7590
-            customRequestParams = ajaxSettings.data || {};
7591
-        }
7592
-        $.extend(params, customRequestParams);
7593
-        params[startParam] = start.format();
7594
-        params[endParam] = end.format();
7595
-        if (timezone && timezone !== 'local') {
7596
-            params[timezoneParam] = timezone;
7597
-        }
7598
-        return params;
7599
-    };
7600
-    JsonFeedEventSource.prototype.getPrimitive = function () {
7601
-        return this.url;
7602
-    };
7603
-    JsonFeedEventSource.prototype.applyMiscProps = function (rawProps) {
7604
-        this.ajaxSettings = rawProps;
7605
-    };
7606
-    JsonFeedEventSource.AJAX_DEFAULTS = {
7607
-        dataType: 'json',
7608
-        cache: false
7609
-    };
7610
-    return JsonFeedEventSource;
7611
-}(EventSource_1.default));
7612
-exports.default = JsonFeedEventSource;
7613
-JsonFeedEventSource.defineStandardProps({
7614
-    // automatically transfer (true)...
7615
-    url: true,
7616
-    startParam: true,
7617
-    endParam: true,
7618
-    timezoneParam: true
7619
-});
7620
-
7621
-
7622
-/***/ }),
7623
-/* 217 */
7624
-/***/ (function(module, exports, __webpack_require__) {
7625
-
7626
-Object.defineProperty(exports, "__esModule", { value: true });
7627
-var EmitterMixin_1 = __webpack_require__(11);
7628
-var TaskQueue = /** @class */ (function () {
7629
-    function TaskQueue() {
7630
-        this.q = [];
7631
-        this.isPaused = false;
7632
-        this.isRunning = false;
7633
-    }
7634
-    TaskQueue.prototype.queue = function () {
7635
-        var args = [];
7636
-        for (var _i = 0; _i < arguments.length; _i++) {
7637
-            args[_i] = arguments[_i];
7638
-        }
7639
-        this.q.push.apply(this.q, args); // append
7640
-        this.tryStart();
7641
-    };
7642
-    TaskQueue.prototype.pause = function () {
7643
-        this.isPaused = true;
7644
-    };
7645
-    TaskQueue.prototype.resume = function () {
7646
-        this.isPaused = false;
7647
-        this.tryStart();
7648
-    };
7649
-    TaskQueue.prototype.getIsIdle = function () {
7650
-        return !this.isRunning && !this.isPaused;
7651
-    };
7652
-    TaskQueue.prototype.tryStart = function () {
7653
-        if (!this.isRunning && this.canRunNext()) {
7654
-            this.isRunning = true;
7655
-            this.trigger('start');
7656
-            this.runRemaining();
7657
-        }
7658
-    };
7659
-    TaskQueue.prototype.canRunNext = function () {
7660
-        return !this.isPaused && this.q.length;
7661
-    };
7662
-    TaskQueue.prototype.runRemaining = function () {
7663
-        var _this = this;
7664
-        var task;
7665
-        var res;
7666
-        do {
7667
-            task = this.q.shift(); // always freshly reference q. might have been reassigned.
7668
-            res = this.runTask(task);
7669
-            if (res && res.then) {
7670
-                res.then(function () {
7671
-                    if (_this.canRunNext()) {
7672
-                        _this.runRemaining();
7673
-                    }
7674
-                });
7675
-                return; // prevent marking as stopped
7676
-            }
7677
-        } while (this.canRunNext());
7678
-        this.trigger('stop'); // not really a 'stop' ... more of a 'drained'
7679
-        this.isRunning = false;
7680
-        // if 'stop' handler added more tasks.... TODO: write test for this
7681
-        this.tryStart();
7682
-    };
7683
-    TaskQueue.prototype.runTask = function (task) {
7684
-        return task(); // task *is* the function, but subclasses can change the format of a task
7685
-    };
7686
-    return TaskQueue;
7687
-}());
7688
-exports.default = TaskQueue;
7689
-EmitterMixin_1.default.mixInto(TaskQueue);
7690
-
7691
-
7692
-/***/ }),
7693
-/* 218 */
7694
-/***/ (function(module, exports, __webpack_require__) {
7695
-
7696
-Object.defineProperty(exports, "__esModule", { value: true });
7697
-var tslib_1 = __webpack_require__(2);
7698
-var TaskQueue_1 = __webpack_require__(217);
7699
-var RenderQueue = /** @class */ (function (_super) {
7700
-    tslib_1.__extends(RenderQueue, _super);
7701
-    function RenderQueue(waitsByNamespace) {
7702
-        var _this = _super.call(this) || this;
7703
-        _this.waitsByNamespace = waitsByNamespace || {};
7704
-        return _this;
7705
-    }
7706
-    RenderQueue.prototype.queue = function (taskFunc, namespace, type) {
7707
-        var task = {
7708
-            func: taskFunc,
7709
-            namespace: namespace,
7710
-            type: type
7711
-        };
7712
-        var waitMs;
7713
-        if (namespace) {
7714
-            waitMs = this.waitsByNamespace[namespace];
7715
-        }
7716
-        if (this.waitNamespace) {
7717
-            if (namespace === this.waitNamespace && waitMs != null) {
7718
-                this.delayWait(waitMs);
7719
-            }
7720
-            else {
7721
-                this.clearWait();
7722
-                this.tryStart();
7723
-            }
7724
-        }
7725
-        if (this.compoundTask(task)) {
7726
-            if (!this.waitNamespace && waitMs != null) {
7727
-                this.startWait(namespace, waitMs);
7728
-            }
7729
-            else {
7730
-                this.tryStart();
7731
-            }
7732
-        }
7733
-    };
7734
-    RenderQueue.prototype.startWait = function (namespace, waitMs) {
7735
-        this.waitNamespace = namespace;
7736
-        this.spawnWait(waitMs);
7737
-    };
7738
-    RenderQueue.prototype.delayWait = function (waitMs) {
7739
-        clearTimeout(this.waitId);
7740
-        this.spawnWait(waitMs);
7741
-    };
7742
-    RenderQueue.prototype.spawnWait = function (waitMs) {
7743
-        var _this = this;
7744
-        this.waitId = setTimeout(function () {
7745
-            _this.waitNamespace = null;
7746
-            _this.tryStart();
7747
-        }, waitMs);
7748
-    };
7749
-    RenderQueue.prototype.clearWait = function () {
7750
-        if (this.waitNamespace) {
7751
-            clearTimeout(this.waitId);
7752
-            this.waitId = null;
7753
-            this.waitNamespace = null;
7754
-        }
7755
-    };
7756
-    RenderQueue.prototype.canRunNext = function () {
7757
-        if (!_super.prototype.canRunNext.call(this)) {
7758
-            return false;
7759
-        }
7760
-        // waiting for a certain namespace to stop receiving tasks?
7761
-        if (this.waitNamespace) {
7762
-            var q = this.q;
7763
-            // if there was a different namespace task in the meantime,
7764
-            // that forces all previously-waiting tasks to suddenly execute.
7765
-            // TODO: find a way to do this in constant time.
7766
-            for (var i = 0; i < q.length; i++) {
7767
-                if (q[i].namespace !== this.waitNamespace) {
7768
-                    return true; // allow execution
7769
-                }
7770
-            }
7771
-            return false;
7772
-        }
7773
-        return true;
7774
-    };
7775
-    RenderQueue.prototype.runTask = function (task) {
7776
-        task.func();
7777
-    };
7778
-    RenderQueue.prototype.compoundTask = function (newTask) {
7779
-        var q = this.q;
7780
-        var shouldAppend = true;
7781
-        var i;
7782
-        var task;
7783
-        if (newTask.namespace && newTask.type === 'destroy') {
7784
-            // remove all init/add/remove ops with same namespace, regardless of order
7785
-            for (i = q.length - 1; i >= 0; i--) {
7786
-                task = q[i];
7787
-                switch (task.type) {
7788
-                    case 'init':
7789
-                        shouldAppend = false;
7790
-                    // the latest destroy is cancelled out by not doing the init
7791
-                    /* falls through */
7792
-                    case 'add':
7793
-                    /* falls through */
7794
-                    case 'remove':
7795
-                        q.splice(i, 1); // remove task
7796
-                }
7797
-            }
7798
-        }
7799
-        if (shouldAppend) {
7800
-            q.push(newTask);
7801
-        }
7802
-        return shouldAppend;
7803
-    };
7804
-    return RenderQueue;
7805
-}(TaskQueue_1.default));
7806
-exports.default = RenderQueue;
7807
-
7808
-
7809
-/***/ }),
7810
-/* 219 */
7811
-/***/ (function(module, exports, __webpack_require__) {
7812
-
7813
-Object.defineProperty(exports, "__esModule", { value: true });
7814
-var tslib_1 = __webpack_require__(2);
7815
-var $ = __webpack_require__(3);
7816
-var moment = __webpack_require__(0);
7817
-var util_1 = __webpack_require__(4);
7818
-var moment_ext_1 = __webpack_require__(10);
7819
-var date_formatting_1 = __webpack_require__(47);
7820
-var Component_1 = __webpack_require__(237);
7821
-var util_2 = __webpack_require__(35);
7822
-var DateComponent = /** @class */ (function (_super) {
7823
-    tslib_1.__extends(DateComponent, _super);
7824
-    function DateComponent(_view, _options) {
7825
-        var _this = _super.call(this) || this;
7826
-        _this.isRTL = false; // frequently accessed options
7827
-        _this.hitsNeededDepth = 0; // necessary because multiple callers might need the same hits
7828
-        _this.hasAllDayBusinessHours = false; // TODO: unify with largeUnit and isTimeScale?
7829
-        _this.isDatesRendered = false;
7830
-        // hack to set options prior to the this.opt calls
7831
-        if (_view) {
7832
-            _this['view'] = _view;
7833
-        }
7834
-        if (_options) {
7835
-            _this['options'] = _options;
7836
-        }
7837
-        _this.uid = String(DateComponent.guid++);
7838
-        _this.childrenByUid = {};
7839
-        _this.nextDayThreshold = moment.duration(_this.opt('nextDayThreshold'));
7840
-        _this.isRTL = _this.opt('isRTL');
7841
-        if (_this.fillRendererClass) {
7842
-            _this.fillRenderer = new _this.fillRendererClass(_this);
7843
-        }
7844
-        if (_this.eventRendererClass) {
7845
-            _this.eventRenderer = new _this.eventRendererClass(_this, _this.fillRenderer);
7846
-        }
7847
-        if (_this.helperRendererClass && _this.eventRenderer) {
7848
-            _this.helperRenderer = new _this.helperRendererClass(_this, _this.eventRenderer);
7849
-        }
7850
-        if (_this.businessHourRendererClass && _this.fillRenderer) {
7851
-            _this.businessHourRenderer = new _this.businessHourRendererClass(_this, _this.fillRenderer);
7852
-        }
7853
-        return _this;
7854
-    }
7855
-    DateComponent.prototype.addChild = function (child) {
7856
-        if (!this.childrenByUid[child.uid]) {
7857
-            this.childrenByUid[child.uid] = child;
7858
-            return true;
7859
-        }
7860
-        return false;
7861
-    };
7862
-    DateComponent.prototype.removeChild = function (child) {
7863
-        if (this.childrenByUid[child.uid]) {
7864
-            delete this.childrenByUid[child.uid];
7865
-            return true;
7866
-        }
7867
-        return false;
7868
-    };
7869
-    // TODO: only do if isInDom?
7870
-    // TODO: make part of Component, along with children/batch-render system?
7871
-    DateComponent.prototype.updateSize = function (totalHeight, isAuto, isResize) {
7872
-        this.callChildren('updateSize', arguments);
7873
-    };
7874
-    // Options
7875
-    // -----------------------------------------------------------------------------------------------------------------
7876
-    DateComponent.prototype.opt = function (name) {
7877
-        return this._getView().opt(name); // default implementation
7878
+    // Retrieves all rendered segment objects currently rendered on the grid
7879
+    DayGrid.prototype.getOwnEventSegs = function () {
7880
+        // append the segments from the "more..." popover
7881
+        return _super.prototype.getOwnEventSegs.call(this).concat(this.popoverSegs || []);
7882
     };
7883
-    DateComponent.prototype.publiclyTrigger = function () {
7884
-        var args = [];
7885
-        for (var _i = 0; _i < arguments.length; _i++) {
7886
-            args[_i] = arguments[_i];
7887
+    /* Event Drag Visualization
7888
+    ------------------------------------------------------------------------------------------------------------------*/
7889
+    // Renders a visual indication of an event or external element being dragged.
7890
+    // `eventLocation` has zoned start and end (optional)
7891
+    DayGrid.prototype.renderDrag = function (eventFootprints, seg, isTouch) {
7892
+        var i;
7893
+        for (i = 0; i < eventFootprints.length; i++) {
7894
+            this.renderHighlight(eventFootprints[i].componentFootprint);
7895
         }
7896
-        var calendar = this._getCalendar();
7897
-        return calendar.publiclyTrigger.apply(calendar, args);
7898
-    };
7899
-    DateComponent.prototype.hasPublicHandlers = function () {
7900
-        var args = [];
7901
-        for (var _i = 0; _i < arguments.length; _i++) {
7902
-            args[_i] = arguments[_i];
7903
+        // render drags from OTHER components as helpers
7904
+        if (eventFootprints.length && seg && seg.component !== this) {
7905
+            this.helperRenderer.renderEventDraggingFootprints(eventFootprints, seg, isTouch);
7906
+            return true; // signal helpers rendered
7907
         }
7908
-        var calendar = this._getCalendar();
7909
-        return calendar.hasPublicHandlers.apply(calendar, args);
7910
-    };
7911
-    // Date
7912
-    // -----------------------------------------------------------------------------------------------------------------
7913
-    DateComponent.prototype.executeDateRender = function (dateProfile) {
7914
-        this.dateProfile = dateProfile; // for rendering
7915
-        this.renderDates(dateProfile);
7916
-        this.isDatesRendered = true;
7917
-        this.callChildren('executeDateRender', arguments);
7918
-    };
7919
-    DateComponent.prototype.executeDateUnrender = function () {
7920
-        this.callChildren('executeDateUnrender', arguments);
7921
-        this.dateProfile = null;
7922
-        this.unrenderDates();
7923
-        this.isDatesRendered = false;
7924
-    };
7925
-    // date-cell content only
7926
-    DateComponent.prototype.renderDates = function (dateProfile) {
7927
-        // subclasses should implement
7928
-    };
7929
-    // date-cell content only
7930
-    DateComponent.prototype.unrenderDates = function () {
7931
-        // subclasses should override
7932
-    };
7933
-    // Now-Indicator
7934
-    // -----------------------------------------------------------------------------------------------------------------
7935
-    // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
7936
-    // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
7937
-    DateComponent.prototype.getNowIndicatorUnit = function () {
7938
-        // subclasses should implement
7939
-    };
7940
-    // Renders a current time indicator at the given datetime
7941
-    DateComponent.prototype.renderNowIndicator = function (date) {
7942
-        this.callChildren('renderNowIndicator', arguments);
7943
     };
7944
-    // Undoes the rendering actions from renderNowIndicator
7945
-    DateComponent.prototype.unrenderNowIndicator = function () {
7946
-        this.callChildren('unrenderNowIndicator', arguments);
7947
+    // Unrenders any visual indication of a hovering event
7948
+    DayGrid.prototype.unrenderDrag = function () {
7949
+        this.unrenderHighlight();
7950
+        this.helperRenderer.unrender();
7951
     };
7952
-    // Business Hours
7953
-    // ---------------------------------------------------------------------------------------------------------------
7954
-    DateComponent.prototype.renderBusinessHours = function (businessHourGenerator) {
7955
-        if (this.businessHourRenderer) {
7956
-            this.businessHourRenderer.render(businessHourGenerator);
7957
+    /* Event Resize Visualization
7958
+    ------------------------------------------------------------------------------------------------------------------*/
7959
+    // Renders a visual indication of an event being resized
7960
+    DayGrid.prototype.renderEventResize = function (eventFootprints, seg, isTouch) {
7961
+        var i;
7962
+        for (i = 0; i < eventFootprints.length; i++) {
7963
+            this.renderHighlight(eventFootprints[i].componentFootprint);
7964
         }
7965
-        this.callChildren('renderBusinessHours', arguments);
7966
+        this.helperRenderer.renderEventResizingFootprints(eventFootprints, seg, isTouch);
7967
     };
7968
-    // Unrenders previously-rendered business-hours
7969
-    DateComponent.prototype.unrenderBusinessHours = function () {
7970
-        this.callChildren('unrenderBusinessHours', arguments);
7971
-        if (this.businessHourRenderer) {
7972
-            this.businessHourRenderer.unrender();
7973
-        }
7974
+    // Unrenders a visual indication of an event being resized
7975
+    DayGrid.prototype.unrenderEventResize = function () {
7976
+        this.unrenderHighlight();
7977
+        this.helperRenderer.unrender();
7978
     };
7979
-    // Event Displaying
7980
-    // -----------------------------------------------------------------------------------------------------------------
7981
-    DateComponent.prototype.executeEventRender = function (eventsPayload) {
7982
-        if (this.eventRenderer) {
7983
-            this.eventRenderer.rangeUpdated(); // poorly named now
7984
-            this.eventRenderer.render(eventsPayload);
7985
+    /* More+ Link Popover
7986
+    ------------------------------------------------------------------------------------------------------------------*/
7987
+    DayGrid.prototype.removeSegPopover = function () {
7988
+        if (this.segPopover) {
7989
+            this.segPopover.hide(); // in handler, will call segPopover's removeElement
7990
         }
7991
-        else if (this['renderEvents']) {
7992
-            this['renderEvents'](convertEventsPayloadToLegacyArray(eventsPayload));
7993
+    };
7994
+    // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
7995
+    // `levelLimit` can be false (don't limit), a number, or true (should be computed).
7996
+    DayGrid.prototype.limitRows = function (levelLimit) {
7997
+        var rowStructs = this.eventRenderer.rowStructs || [];
7998
+        var row; // row #
7999
+        var rowLevelLimit;
8000
+        for (row = 0; row < rowStructs.length; row++) {
8001
+            this.unlimitRow(row);
8002
+            if (!levelLimit) {
8003
+                rowLevelLimit = false;
8004
+            }
8005
+            else if (typeof levelLimit === 'number') {
8006
+                rowLevelLimit = levelLimit;
8007
+            }
8008
+            else {
8009
+                rowLevelLimit = this.computeRowLevelLimit(row);
8010
+            }
8011
+            if (rowLevelLimit !== false) {
8012
+                this.limitRow(row, rowLevelLimit);
8013
+            }
8014
         }
8015
-        this.callChildren('executeEventRender', arguments);
8016
     };
8017
-    DateComponent.prototype.executeEventUnrender = function () {
8018
-        this.callChildren('executeEventUnrender', arguments);
8019
-        if (this.eventRenderer) {
8020
-            this.eventRenderer.unrender();
8021
+    // Computes the number of levels a row will accomodate without going outside its bounds.
8022
+    // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
8023
+    // `row` is the row number.
8024
+    DayGrid.prototype.computeRowLevelLimit = function (row) {
8025
+        var rowEl = this.rowEls.eq(row); // the containing "fake" row div
8026
+        var rowHeight = rowEl.height(); // TODO: cache somehow?
8027
+        var trEls = this.eventRenderer.rowStructs[row].tbodyEl.children();
8028
+        var i;
8029
+        var trEl;
8030
+        var trHeight;
8031
+        function iterInnerHeights(i, childNode) {
8032
+            trHeight = Math.max(trHeight, $(childNode).outerHeight());
8033
         }
8034
-        else if (this['destroyEvents']) {
8035
-            this['destroyEvents']();
8036
+        // Reveal one level <tr> at a time and stop when we find one out of bounds
8037
+        for (i = 0; i < trEls.length; i++) {
8038
+            trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal)
8039
+            // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
8040
+            // so instead, find the tallest inner content element.
8041
+            trHeight = 0;
8042
+            trEl.find('> td > :first-child').each(iterInnerHeights);
8043
+            if (trEl.position().top + trHeight > rowHeight) {
8044
+                return i;
8045
+            }
8046
         }
8047
+        return false; // should not limit at all
8048
     };
8049
-    DateComponent.prototype.getBusinessHourSegs = function () {
8050
-        var segs = this.getOwnBusinessHourSegs();
8051
-        this.iterChildren(function (child) {
8052
-            segs.push.apply(segs, child.getBusinessHourSegs());
8053
-        });
8054
-        return segs;
8055
-    };
8056
-    DateComponent.prototype.getOwnBusinessHourSegs = function () {
8057
-        if (this.businessHourRenderer) {
8058
-            return this.businessHourRenderer.getSegs();
8059
+    // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
8060
+    // `row` is the row number.
8061
+    // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
8062
+    DayGrid.prototype.limitRow = function (row, levelLimit) {
8063
+        var _this = this;
8064
+        var rowStruct = this.eventRenderer.rowStructs[row];
8065
+        var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
8066
+        var col = 0; // col #, left-to-right (not chronologically)
8067
+        var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
8068
+        var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
8069
+        var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
8070
+        var i;
8071
+        var seg;
8072
+        var segsBelow; // array of segment objects below `seg` in the current `col`
8073
+        var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
8074
+        var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
8075
+        var td;
8076
+        var rowspan;
8077
+        var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
8078
+        var j;
8079
+        var moreTd;
8080
+        var moreWrap;
8081
+        var moreLink;
8082
+        // Iterates through empty level cells and places "more" links inside if need be
8083
+        var emptyCellsUntil = function (endCol) {
8084
+            while (col < endCol) {
8085
+                segsBelow = _this.getCellSegs(row, col, levelLimit);
8086
+                if (segsBelow.length) {
8087
+                    td = cellMatrix[levelLimit - 1][col];
8088
+                    moreLink = _this.renderMoreLink(row, col, segsBelow);
8089
+                    moreWrap = $('<div>').append(moreLink);
8090
+                    td.append(moreWrap);
8091
+                    moreNodes.push(moreWrap[0]);
8092
+                }
8093
+                col++;
8094
+            }
8095
+        };
8096
+        if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
8097
+            levelSegs = rowStruct.segLevels[levelLimit - 1];
8098
+            cellMatrix = rowStruct.cellMatrix;
8099
+            limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
8100
+                .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
8101
+            // iterate though segments in the last allowable level
8102
+            for (i = 0; i < levelSegs.length; i++) {
8103
+                seg = levelSegs[i];
8104
+                emptyCellsUntil(seg.leftCol); // process empty cells before the segment
8105
+                // determine *all* segments below `seg` that occupy the same columns
8106
+                colSegsBelow = [];
8107
+                totalSegsBelow = 0;
8108
+                while (col <= seg.rightCol) {
8109
+                    segsBelow = this.getCellSegs(row, col, levelLimit);
8110
+                    colSegsBelow.push(segsBelow);
8111
+                    totalSegsBelow += segsBelow.length;
8112
+                    col++;
8113
+                }
8114
+                if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
8115
+                    td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
8116
+                    rowspan = td.attr('rowspan') || 1;
8117
+                    segMoreNodes = [];
8118
+                    // make a replacement <td> for each column the segment occupies. will be one for each colspan
8119
+                    for (j = 0; j < colSegsBelow.length; j++) {
8120
+                        moreTd = $('<td class="fc-more-cell">').attr('rowspan', rowspan);
8121
+                        segsBelow = colSegsBelow[j];
8122
+                        moreLink = this.renderMoreLink(row, seg.leftCol + j, [seg].concat(segsBelow) // count seg as hidden too
8123
+                        );
8124
+                        moreWrap = $('<div>').append(moreLink);
8125
+                        moreTd.append(moreWrap);
8126
+                        segMoreNodes.push(moreTd[0]);
8127
+                        moreNodes.push(moreTd[0]);
8128
+                    }
8129
+                    td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
8130
+                    limitedNodes.push(td[0]);
8131
+                }
8132
+            }
8133
+            emptyCellsUntil(this.colCnt); // finish off the level
8134
+            rowStruct.moreEls = $(moreNodes); // for easy undoing later
8135
+            rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
8136
         }
8137
-        return [];
8138
-    };
8139
-    DateComponent.prototype.getEventSegs = function () {
8140
-        var segs = this.getOwnEventSegs();
8141
-        this.iterChildren(function (child) {
8142
-            segs.push.apply(segs, child.getEventSegs());
8143
-        });
8144
-        return segs;
8145
     };
8146
-    DateComponent.prototype.getOwnEventSegs = function () {
8147
-        if (this.eventRenderer) {
8148
-            return this.eventRenderer.getSegs();
8149
+    // Reveals all levels and removes all "more"-related elements for a grid's row.
8150
+    // `row` is a row number.
8151
+    DayGrid.prototype.unlimitRow = function (row) {
8152
+        var rowStruct = this.eventRenderer.rowStructs[row];
8153
+        if (rowStruct.moreEls) {
8154
+            rowStruct.moreEls.remove();
8155
+            rowStruct.moreEls = null;
8156
         }
8157
-        return [];
8158
-    };
8159
-    // Event Rendering Triggering
8160
-    // -----------------------------------------------------------------------------------------------------------------
8161
-    DateComponent.prototype.triggerAfterEventsRendered = function () {
8162
-        this.triggerAfterEventSegsRendered(this.getEventSegs());
8163
-        this.publiclyTrigger('eventAfterAllRender', {
8164
-            context: this,
8165
-            args: [this]
8166
-        });
8167
-    };
8168
-    DateComponent.prototype.triggerAfterEventSegsRendered = function (segs) {
8169
-        var _this = this;
8170
-        // an optimization, because getEventLegacy is expensive
8171
-        if (this.hasPublicHandlers('eventAfterRender')) {
8172
-            segs.forEach(function (seg) {
8173
-                var legacy;
8174
-                if (seg.el) {
8175
-                    legacy = seg.footprint.getEventLegacy();
8176
-                    _this.publiclyTrigger('eventAfterRender', {
8177
-                        context: legacy,
8178
-                        args: [legacy, seg.el, _this]
8179
-                    });
8180
-                }
8181
-            });
8182
+        if (rowStruct.limitedEls) {
8183
+            rowStruct.limitedEls.removeClass('fc-limited');
8184
+            rowStruct.limitedEls = null;
8185
         }
8186
     };
8187
-    DateComponent.prototype.triggerBeforeEventsDestroyed = function () {
8188
-        this.triggerBeforeEventSegsDestroyed(this.getEventSegs());
8189
-    };
8190
-    DateComponent.prototype.triggerBeforeEventSegsDestroyed = function (segs) {
8191
+    // Renders an <a> element that represents hidden event element for a cell.
8192
+    // Responsible for attaching click handler as well.
8193
+    DayGrid.prototype.renderMoreLink = function (row, col, hiddenSegs) {
8194
         var _this = this;
8195
-        if (this.hasPublicHandlers('eventDestroy')) {
8196
-            segs.forEach(function (seg) {
8197
-                var legacy;
8198
-                if (seg.el) {
8199
-                    legacy = seg.footprint.getEventLegacy();
8200
-                    _this.publiclyTrigger('eventDestroy', {
8201
-                        context: legacy,
8202
-                        args: [legacy, seg.el, _this]
8203
-                    });
8204
-                }
8205
-            });
8206
-        }
8207
-    };
8208
-    // Event Rendering Utils
8209
-    // -----------------------------------------------------------------------------------------------------------------
8210
-    // Hides all rendered event segments linked to the given event
8211
-    // RECURSIVE with subcomponents
8212
-    DateComponent.prototype.showEventsWithId = function (eventDefId) {
8213
-        this.getEventSegs().forEach(function (seg) {
8214
-            if (seg.footprint.eventDef.id === eventDefId &&
8215
-                seg.el // necessary?
8216
-            ) {
8217
-                seg.el.css('visibility', '');
8218
+        var view = this.view;
8219
+        return $('<a class="fc-more">')
8220
+            .text(this.getMoreLinkText(hiddenSegs.length))
8221
+            .on('click', function (ev) {
8222
+            var clickOption = _this.opt('eventLimitClick');
8223
+            var date = _this.getCellDate(row, col);
8224
+            var moreEl = $(ev.currentTarget);
8225
+            var dayEl = _this.getCellEl(row, col);
8226
+            var allSegs = _this.getCellSegs(row, col);
8227
+            // rescope the segments to be within the cell's date
8228
+            var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
8229
+            var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
8230
+            if (typeof clickOption === 'function') {
8231
+                // the returned value can be an atomic option
8232
+                clickOption = _this.publiclyTrigger('eventLimitClick', {
8233
+                    context: view,
8234
+                    args: [
8235
+                        {
8236
+                            date: date.clone(),
8237
+                            dayEl: dayEl,
8238
+                            moreEl: moreEl,
8239
+                            segs: reslicedAllSegs,
8240
+                            hiddenSegs: reslicedHiddenSegs
8241
+                        },
8242
+                        ev,
8243
+                        view
8244
+                    ]
8245
+                });
8246
             }
8247
-        });
8248
-        this.callChildren('showEventsWithId', arguments);
8249
-    };
8250
-    // Shows all rendered event segments linked to the given event
8251
-    // RECURSIVE with subcomponents
8252
-    DateComponent.prototype.hideEventsWithId = function (eventDefId) {
8253
-        this.getEventSegs().forEach(function (seg) {
8254
-            if (seg.footprint.eventDef.id === eventDefId &&
8255
-                seg.el // necessary?
8256
-            ) {
8257
-                seg.el.css('visibility', 'hidden');
8258
+            if (clickOption === 'popover') {
8259
+                _this.showSegPopover(row, col, moreEl, reslicedAllSegs);
8260
             }
8261
-        });
8262
-        this.callChildren('hideEventsWithId', arguments);
8263
-    };
8264
-    // Drag-n-Drop Rendering (for both events and external elements)
8265
-    // ---------------------------------------------------------------------------------------------------------------
8266
-    // Renders a visual indication of a event or external-element drag over the given drop zone.
8267
-    // If an external-element, seg will be `null`.
8268
-    // Must return elements used for any mock events.
8269
-    DateComponent.prototype.renderDrag = function (eventFootprints, seg, isTouch) {
8270
-        var renderedHelper = false;
8271
-        this.iterChildren(function (child) {
8272
-            if (child.renderDrag(eventFootprints, seg, isTouch)) {
8273
-                renderedHelper = true;
8274
+            else if (typeof clickOption === 'string') { // a view name
8275
+                view.calendar.zoomTo(date, clickOption);
8276
             }
8277
         });
8278
-        return renderedHelper;
8279
-    };
8280
-    // Unrenders a visual indication of an event or external-element being dragged.
8281
-    DateComponent.prototype.unrenderDrag = function () {
8282
-        this.callChildren('unrenderDrag', arguments);
8283
-    };
8284
-    // Event Resizing
8285
-    // ---------------------------------------------------------------------------------------------------------------
8286
-    // Renders a visual indication of an event being resized.
8287
-    DateComponent.prototype.renderEventResize = function (eventFootprints, seg, isTouch) {
8288
-        this.callChildren('renderEventResize', arguments);
8289
-    };
8290
-    // Unrenders a visual indication of an event being resized.
8291
-    DateComponent.prototype.unrenderEventResize = function () {
8292
-        this.callChildren('unrenderEventResize', arguments);
8293
-    };
8294
-    // Selection
8295
-    // ---------------------------------------------------------------------------------------------------------------
8296
-    // Renders a visual indication of the selection
8297
-    // TODO: rename to `renderSelection` after legacy is gone
8298
-    DateComponent.prototype.renderSelectionFootprint = function (componentFootprint) {
8299
-        this.renderHighlight(componentFootprint);
8300
-        this.callChildren('renderSelectionFootprint', arguments);
8301
-    };
8302
-    // Unrenders a visual indication of selection
8303
-    DateComponent.prototype.unrenderSelection = function () {
8304
-        this.unrenderHighlight();
8305
-        this.callChildren('unrenderSelection', arguments);
8306
     };
8307
-    // Highlight
8308
-    // ---------------------------------------------------------------------------------------------------------------
8309
-    // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
8310
-    DateComponent.prototype.renderHighlight = function (componentFootprint) {
8311
-        if (this.fillRenderer) {
8312
-            this.fillRenderer.renderFootprint('highlight', componentFootprint, {
8313
-                getClasses: function () {
8314
-                    return ['fc-highlight'];
8315
-                }
8316
-            });
8317
+    // Reveals the popover that displays all events within a cell
8318
+    DayGrid.prototype.showSegPopover = function (row, col, moreLink, segs) {
8319
+        var _this = this;
8320
+        var view = this.view;
8321
+        var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
8322
+        var topEl; // the element we want to match the top coordinate of
8323
+        var options;
8324
+        if (this.rowCnt === 1) {
8325
+            topEl = view.el; // will cause the popover to cover any sort of header
8326
         }
8327
-        this.callChildren('renderHighlight', arguments);
8328
-    };
8329
-    // Unrenders the emphasis on a date range
8330
-    DateComponent.prototype.unrenderHighlight = function () {
8331
-        if (this.fillRenderer) {
8332
-            this.fillRenderer.unrender('highlight');
8333
+        else {
8334
+            topEl = this.rowEls.eq(row); // will align with top of row
8335
         }
8336
-        this.callChildren('unrenderHighlight', arguments);
8337
-    };
8338
-    // Hit Areas
8339
-    // ---------------------------------------------------------------------------------------------------------------
8340
-    // just because all DateComponents support this interface
8341
-    // doesn't mean they need to have their own internal coord system. they can defer to sub-components.
8342
-    DateComponent.prototype.hitsNeeded = function () {
8343
-        if (!(this.hitsNeededDepth++)) {
8344
-            this.prepareHits();
8345
+        options = {
8346
+            className: 'fc-more-popover ' + view.calendar.theme.getClass('popover'),
8347
+            content: this.renderSegPopoverContent(row, col, segs),
8348
+            parentEl: view.el,
8349
+            top: topEl.offset().top,
8350
+            autoHide: true,
8351
+            viewportConstrain: this.opt('popoverViewportConstrain'),
8352
+            hide: function () {
8353
+                // kill everything when the popover is hidden
8354
+                // notify events to be removed
8355
+                if (_this.popoverSegs) {
8356
+                    _this.triggerBeforeEventSegsDestroyed(_this.popoverSegs);
8357
+                }
8358
+                _this.segPopover.removeElement();
8359
+                _this.segPopover = null;
8360
+                _this.popoverSegs = null;
8361
+            }
8362
+        };
8363
+        // Determine horizontal coordinate.
8364
+        // We use the moreWrap instead of the <td> to avoid border confusion.
8365
+        if (this.isRTL) {
8366
+            options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
8367
         }
8368
-        this.callChildren('hitsNeeded', arguments);
8369
+        else {
8370
+            options.left = moreWrap.offset().left - 1; // -1 to be over cell border
8371
+        }
8372
+        this.segPopover = new Popover_1.default(options);
8373
+        this.segPopover.show();
8374
+        // the popover doesn't live within the grid's container element, and thus won't get the event
8375
+        // delegated-handlers for free. attach event-related handlers to the popover.
8376
+        this.bindAllSegHandlersToEl(this.segPopover.el);
8377
+        this.triggerAfterEventSegsRendered(segs);
8378
     };
8379
-    DateComponent.prototype.hitsNotNeeded = function () {
8380
-        if (this.hitsNeededDepth && !(--this.hitsNeededDepth)) {
8381
-            this.releaseHits();
8382
+    // Builds the inner DOM contents of the segment popover
8383
+    DayGrid.prototype.renderSegPopoverContent = function (row, col, segs) {
8384
+        var view = this.view;
8385
+        var theme = view.calendar.theme;
8386
+        var title = this.getCellDate(row, col).format(this.opt('dayPopoverFormat'));
8387
+        var content = $('<div class="fc-header ' + theme.getClass('popoverHeader') + '">' +
8388
+            '<span class="fc-close ' + theme.getIconClass('close') + '"></span>' +
8389
+            '<span class="fc-title">' +
8390
+            util_1.htmlEscape(title) +
8391
+            '</span>' +
8392
+            '<div class="fc-clear"></div>' +
8393
+            '</div>' +
8394
+            '<div class="fc-body ' + theme.getClass('popoverContent') + '">' +
8395
+            '<div class="fc-event-container"></div>' +
8396
+            '</div>');
8397
+        var segContainer = content.find('.fc-event-container');
8398
+        var i;
8399
+        // render each seg's `el` and only return the visible segs
8400
+        segs = this.eventRenderer.renderFgSegEls(segs, true); // disableResizing=true
8401
+        this.popoverSegs = segs;
8402
+        for (i = 0; i < segs.length; i++) {
8403
+            // because segments in the popover are not part of a grid coordinate system, provide a hint to any
8404
+            // grids that want to do drag-n-drop about which cell it came from
8405
+            this.hitsNeeded();
8406
+            segs[i].hit = this.getCellHit(row, col);
8407
+            this.hitsNotNeeded();
8408
+            segContainer.append(segs[i].el);
8409
         }
8410
-        this.callChildren('hitsNotNeeded', arguments);
8411
-    };
8412
-    DateComponent.prototype.prepareHits = function () {
8413
-        // subclasses can implement
8414
-    };
8415
-    DateComponent.prototype.releaseHits = function () {
8416
-        // subclasses can implement
8417
+        return content;
8418
     };
8419
-    // Given coordinates from the topleft of the document, return data about the date-related area underneath.
8420
-    // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
8421
-    // Must have a `grid` property, a reference to this current grid. TODO: avoid this
8422
-    // The returned object will be processed by getHitFootprint and getHitEl.
8423
-    DateComponent.prototype.queryHit = function (leftOffset, topOffset) {
8424
-        var childrenByUid = this.childrenByUid;
8425
-        var uid;
8426
-        var hit;
8427
-        for (uid in childrenByUid) {
8428
-            hit = childrenByUid[uid].queryHit(leftOffset, topOffset);
8429
-            if (hit) {
8430
-                break;
8431
+    // Given the events within an array of segment objects, reslice them to be in a single day
8432
+    DayGrid.prototype.resliceDaySegs = function (segs, dayDate) {
8433
+        var dayStart = dayDate.clone();
8434
+        var dayEnd = dayStart.clone().add(1, 'days');
8435
+        var dayRange = new UnzonedRange_1.default(dayStart, dayEnd);
8436
+        var newSegs = [];
8437
+        var i;
8438
+        var seg;
8439
+        var slicedRange;
8440
+        for (i = 0; i < segs.length; i++) {
8441
+            seg = segs[i];
8442
+            slicedRange = seg.footprint.componentFootprint.unzonedRange.intersect(dayRange);
8443
+            if (slicedRange) {
8444
+                newSegs.push($.extend({}, seg, {
8445
+                    footprint: new EventFootprint_1.default(new ComponentFootprint_1.default(slicedRange, seg.footprint.componentFootprint.isAllDay), seg.footprint.eventDef, seg.footprint.eventInstance),
8446
+                    isStart: seg.isStart && slicedRange.isStart,
8447
+                    isEnd: seg.isEnd && slicedRange.isEnd
8448
+                }));
8449
             }
8450
         }
8451
-        return hit;
8452
+        // force an order because eventsToSegs doesn't guarantee one
8453
+        // TODO: research if still needed
8454
+        this.eventRenderer.sortEventSegs(newSegs);
8455
+        return newSegs;
8456
     };
8457
-    DateComponent.prototype.getSafeHitFootprint = function (hit) {
8458
-        var footprint = this.getHitFootprint(hit);
8459
-        if (!this.dateProfile.activeUnzonedRange.containsRange(footprint.unzonedRange)) {
8460
-            return null;
8461
+    // Generates the text that should be inside a "more" link, given the number of events it represents
8462
+    DayGrid.prototype.getMoreLinkText = function (num) {
8463
+        var opt = this.opt('eventLimitText');
8464
+        if (typeof opt === 'function') {
8465
+            return opt(num);
8466
         }
8467
-        return footprint;
8468
-    };
8469
-    DateComponent.prototype.getHitFootprint = function (hit) {
8470
-        // what about being abstract!?
8471
-    };
8472
-    // Given position-level information about a date-related area within the grid,
8473
-    // should return a jQuery element that best represents it. passed to dayClick callback.
8474
-    DateComponent.prototype.getHitEl = function (hit) {
8475
-        // what about being abstract!?
8476
-    };
8477
-    /* Converting eventRange -> eventFootprint
8478
-    ------------------------------------------------------------------------------------------------------------------*/
8479
-    DateComponent.prototype.eventRangesToEventFootprints = function (eventRanges) {
8480
-        var eventFootprints = [];
8481
-        var i;
8482
-        for (i = 0; i < eventRanges.length; i++) {
8483
-            eventFootprints.push.apply(// append
8484
-            eventFootprints, this.eventRangeToEventFootprints(eventRanges[i]));
8485
+        else {
8486
+            return '+' + num + ' ' + opt;
8487
         }
8488
-        return eventFootprints;
8489
     };
8490
-    DateComponent.prototype.eventRangeToEventFootprints = function (eventRange) {
8491
-        return [util_2.eventRangeToEventFootprint(eventRange)];
8492
-    };
8493
-    /* Converting componentFootprint/eventFootprint -> segs
8494
-    ------------------------------------------------------------------------------------------------------------------*/
8495
-    DateComponent.prototype.eventFootprintsToSegs = function (eventFootprints) {
8496
+    // Returns segments within a given cell.
8497
+    // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
8498
+    DayGrid.prototype.getCellSegs = function (row, col, startLevel) {
8499
+        var segMatrix = this.eventRenderer.rowStructs[row].segMatrix;
8500
+        var level = startLevel || 0;
8501
         var segs = [];
8502
-        var i;
8503
-        for (i = 0; i < eventFootprints.length; i++) {
8504
-            segs.push.apply(segs, this.eventFootprintToSegs(eventFootprints[i]));
8505
+        var seg;
8506
+        while (level < segMatrix.length) {
8507
+            seg = segMatrix[level][col];
8508
+            if (seg) {
8509
+                segs.push(seg);
8510
+            }
8511
+            level++;
8512
         }
8513
         return segs;
8514
     };
8515
-    // Given an event's span (unzoned start/end and other misc data), and the event itself,
8516
-    // slices into segments and attaches event-derived properties to them.
8517
-    // eventSpan - { start, end, isStart, isEnd, otherthings... }
8518
-    DateComponent.prototype.eventFootprintToSegs = function (eventFootprint) {
8519
-        var unzonedRange = eventFootprint.componentFootprint.unzonedRange;
8520
-        var segs;
8521
-        var i;
8522
-        var seg;
8523
-        segs = this.componentFootprintToSegs(eventFootprint.componentFootprint);
8524
-        for (i = 0; i < segs.length; i++) {
8525
-            seg = segs[i];
8526
-            if (!unzonedRange.isStart) {
8527
-                seg.isStart = false;
8528
+    return DayGrid;
8529
+}(InteractiveDateComponent_1.default));
8530
+exports.default = DayGrid;
8531
+DayGrid.prototype.eventRendererClass = DayGridEventRenderer_1.default;
8532
+DayGrid.prototype.businessHourRendererClass = BusinessHourRenderer_1.default;
8533
+DayGrid.prototype.helperRendererClass = DayGridHelperRenderer_1.default;
8534
+DayGrid.prototype.fillRendererClass = DayGridFillRenderer_1.default;
8535
+StandardInteractionsMixin_1.default.mixInto(DayGrid);
8536
+DayTableMixin_1.default.mixInto(DayGrid);
8537
+
8538
+
8539
+/***/ }),
8540
+/* 67 */
8541
+/***/ (function(module, exports, __webpack_require__) {
8542
+
8543
+Object.defineProperty(exports, "__esModule", { value: true });
8544
+var tslib_1 = __webpack_require__(2);
8545
+var $ = __webpack_require__(3);
8546
+var util_1 = __webpack_require__(4);
8547
+var Scroller_1 = __webpack_require__(41);
8548
+var View_1 = __webpack_require__(43);
8549
+var BasicViewDateProfileGenerator_1 = __webpack_require__(68);
8550
+var DayGrid_1 = __webpack_require__(66);
8551
+/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
8552
+----------------------------------------------------------------------------------------------------------------------*/
8553
+// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
8554
+// It is responsible for managing width/height.
8555
+var BasicView = /** @class */ (function (_super) {
8556
+    tslib_1.__extends(BasicView, _super);
8557
+    function BasicView(calendar, viewSpec) {
8558
+        var _this = _super.call(this, calendar, viewSpec) || this;
8559
+        _this.dayGrid = _this.instantiateDayGrid();
8560
+        _this.dayGrid.isRigid = _this.hasRigidRows();
8561
+        if (_this.opt('weekNumbers')) {
8562
+            if (_this.opt('weekNumbersWithinDays')) {
8563
+                _this.dayGrid.cellWeekNumbersVisible = true;
8564
+                _this.dayGrid.colWeekNumbersVisible = false;
8565
             }
8566
-            if (!unzonedRange.isEnd) {
8567
-                seg.isEnd = false;
8568
+            else {
8569
+                _this.dayGrid.cellWeekNumbersVisible = false;
8570
+                _this.dayGrid.colWeekNumbersVisible = true;
8571
             }
8572
-            seg.footprint = eventFootprint;
8573
-            // TODO: rename to seg.eventFootprint
8574
         }
8575
-        return segs;
8576
+        _this.addChild(_this.dayGrid);
8577
+        _this.scroller = new Scroller_1.default({
8578
+            overflowX: 'hidden',
8579
+            overflowY: 'auto'
8580
+        });
8581
+        return _this;
8582
+    }
8583
+    // Generates the DayGrid object this view needs. Draws from this.dayGridClass
8584
+    BasicView.prototype.instantiateDayGrid = function () {
8585
+        // generate a subclass on the fly with BasicView-specific behavior
8586
+        // TODO: cache this subclass
8587
+        var subclass = makeDayGridSubclass(this.dayGridClass);
8588
+        return new subclass(this);
8589
     };
8590
-    DateComponent.prototype.componentFootprintToSegs = function (componentFootprint) {
8591
-        return [];
8592
+    BasicView.prototype.executeDateRender = function (dateProfile) {
8593
+        this.dayGrid.breakOnWeeks = /year|month|week/.test(dateProfile.currentRangeUnit);
8594
+        _super.prototype.executeDateRender.call(this, dateProfile);
8595
     };
8596
-    // Utils
8597
-    // ---------------------------------------------------------------------------------------------------------------
8598
-    DateComponent.prototype.callChildren = function (methodName, args) {
8599
-        this.iterChildren(function (child) {
8600
-            child[methodName].apply(child, args);
8601
-        });
8602
+    BasicView.prototype.renderSkeleton = function () {
8603
+        var dayGridContainerEl;
8604
+        var dayGridEl;
8605
+        this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml());
8606
+        this.scroller.render();
8607
+        dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container');
8608
+        dayGridEl = $('<div class="fc-day-grid">').appendTo(dayGridContainerEl);
8609
+        this.el.find('.fc-body > tr > td').append(dayGridContainerEl);
8610
+        this.dayGrid.headContainerEl = this.el.find('.fc-head-container');
8611
+        this.dayGrid.setElement(dayGridEl);
8612
     };
8613
-    DateComponent.prototype.iterChildren = function (func) {
8614
-        var childrenByUid = this.childrenByUid;
8615
-        var uid;
8616
-        for (uid in childrenByUid) {
8617
-            func(childrenByUid[uid]);
8618
-        }
8619
+    BasicView.prototype.unrenderSkeleton = function () {
8620
+        this.dayGrid.removeElement();
8621
+        this.scroller.destroy();
8622
     };
8623
-    DateComponent.prototype._getCalendar = function () {
8624
-        var t = this;
8625
-        return t.calendar || t.view.calendar;
8626
+    // Builds the HTML skeleton for the view.
8627
+    // The day-grid component will render inside of a container defined by this HTML.
8628
+    BasicView.prototype.renderSkeletonHtml = function () {
8629
+        var theme = this.calendar.theme;
8630
+        return '' +
8631
+            '<table class="' + theme.getClass('tableGrid') + '">' +
8632
+            (this.opt('columnHeader') ?
8633
+                '<thead class="fc-head">' +
8634
+                    '<tr>' +
8635
+                    '<td class="fc-head-container ' + theme.getClass('widgetHeader') + '">&nbsp;</td>' +
8636
+                    '</tr>' +
8637
+                    '</thead>' :
8638
+                '') +
8639
+            '<tbody class="fc-body">' +
8640
+            '<tr>' +
8641
+            '<td class="' + theme.getClass('widgetContent') + '"></td>' +
8642
+            '</tr>' +
8643
+            '</tbody>' +
8644
+            '</table>';
8645
     };
8646
-    DateComponent.prototype._getView = function () {
8647
-        return this.view;
8648
+    // Generates an HTML attribute string for setting the width of the week number column, if it is known
8649
+    BasicView.prototype.weekNumberStyleAttr = function () {
8650
+        if (this.weekNumberWidth != null) {
8651
+            return 'style="width:' + this.weekNumberWidth + 'px"';
8652
+        }
8653
+        return '';
8654
     };
8655
-    DateComponent.prototype._getDateProfile = function () {
8656
-        return this._getView().get('dateProfile');
8657
+    // Determines whether each row should have a constant height
8658
+    BasicView.prototype.hasRigidRows = function () {
8659
+        var eventLimit = this.opt('eventLimit');
8660
+        return eventLimit && typeof eventLimit !== 'number';
8661
     };
8662
-    // Generates HTML for an anchor to another view into the calendar.
8663
-    // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
8664
-    // `gotoOptions` can either be a moment input, or an object with the form:
8665
-    // { date, type, forceOff }
8666
-    // `type` is a view-type like "day" or "week". default value is "day".
8667
-    // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
8668
-    DateComponent.prototype.buildGotoAnchorHtml = function (gotoOptions, attrs, innerHtml) {
8669
-        var date;
8670
-        var type;
8671
-        var forceOff;
8672
-        var finalOptions;
8673
-        if ($.isPlainObject(gotoOptions)) {
8674
-            date = gotoOptions.date;
8675
-            type = gotoOptions.type;
8676
-            forceOff = gotoOptions.forceOff;
8677
+    /* Dimensions
8678
+    ------------------------------------------------------------------------------------------------------------------*/
8679
+    // Refreshes the horizontal dimensions of the view
8680
+    BasicView.prototype.updateSize = function (totalHeight, isAuto, isResize) {
8681
+        var eventLimit = this.opt('eventLimit');
8682
+        var headRowEl = this.dayGrid.headContainerEl.find('.fc-row');
8683
+        var scrollerHeight;
8684
+        var scrollbarWidths;
8685
+        // hack to give the view some height prior to dayGrid's columns being rendered
8686
+        // TODO: separate setting height from scroller VS dayGrid.
8687
+        if (!this.dayGrid.rowEls) {
8688
+            if (!isAuto) {
8689
+                scrollerHeight = this.computeScrollerHeight(totalHeight);
8690
+                this.scroller.setHeight(scrollerHeight);
8691
+            }
8692
+            return;
8693
         }
8694
-        else {
8695
-            date = gotoOptions; // a single moment input
8696
+        _super.prototype.updateSize.call(this, totalHeight, isAuto, isResize);
8697
+        if (this.dayGrid.colWeekNumbersVisible) {
8698
+            // Make sure all week number cells running down the side have the same width.
8699
+            // Record the width for cells created later.
8700
+            this.weekNumberWidth = util_1.matchCellWidths(this.el.find('.fc-week-number'));
8701
         }
8702
-        date = moment_ext_1.default(date); // if a string, parse it
8703
-        finalOptions = {
8704
-            date: date.format('YYYY-MM-DD'),
8705
-            type: type || 'day'
8706
-        };
8707
-        if (typeof attrs === 'string') {
8708
-            innerHtml = attrs;
8709
-            attrs = null;
8710
+        // reset all heights to be natural
8711
+        this.scroller.clear();
8712
+        util_1.uncompensateScroll(headRowEl);
8713
+        this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
8714
+        // is the event limit a constant level number?
8715
+        if (eventLimit && typeof eventLimit === 'number') {
8716
+            this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
8717
         }
8718
-        attrs = attrs ? ' ' + util_1.attrsToStr(attrs) : ''; // will have a leading space
8719
-        innerHtml = innerHtml || '';
8720
-        if (!forceOff && this.opt('navLinks')) {
8721
-            return '<a' + attrs +
8722
-                ' data-goto="' + util_1.htmlEscape(JSON.stringify(finalOptions)) + '">' +
8723
-                innerHtml +
8724
-                '</a>';
8725
+        // distribute the height to the rows
8726
+        // (totalHeight is a "recommended" value if isAuto)
8727
+        scrollerHeight = this.computeScrollerHeight(totalHeight);
8728
+        this.setGridHeight(scrollerHeight, isAuto);
8729
+        // is the event limit dynamically calculated?
8730
+        if (eventLimit && typeof eventLimit !== 'number') {
8731
+            this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
8732
         }
8733
-        else {
8734
-            return '<span' + attrs + '>' +
8735
-                innerHtml +
8736
-                '</span>';
8737
+        if (!isAuto) { // should we force dimensions of the scroll container?
8738
+            this.scroller.setHeight(scrollerHeight);
8739
+            scrollbarWidths = this.scroller.getScrollbarWidths();
8740
+            if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
8741
+                util_1.compensateScroll(headRowEl, scrollbarWidths);
8742
+                // doing the scrollbar compensation might have created text overflow which created more height. redo
8743
+                scrollerHeight = this.computeScrollerHeight(totalHeight);
8744
+                this.scroller.setHeight(scrollerHeight);
8745
+            }
8746
+            // guarantees the same scrollbar widths
8747
+            this.scroller.lockOverflow(scrollbarWidths);
8748
         }
8749
     };
8750
-    DateComponent.prototype.getAllDayHtml = function () {
8751
-        return this.opt('allDayHtml') || util_1.htmlEscape(this.opt('allDayText'));
8752
+    // given a desired total height of the view, returns what the height of the scroller should be
8753
+    BasicView.prototype.computeScrollerHeight = function (totalHeight) {
8754
+        return totalHeight -
8755
+            util_1.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
8756
     };
8757
-    // Computes HTML classNames for a single-day element
8758
-    DateComponent.prototype.getDayClasses = function (date, noThemeHighlight) {
8759
-        var view = this._getView();
8760
-        var classes = [];
8761
-        var today;
8762
-        if (!this.dateProfile.activeUnzonedRange.containsDate(date)) {
8763
-            classes.push('fc-disabled-day'); // TODO: jQuery UI theme?
8764
+    // Sets the height of just the DayGrid component in this view
8765
+    BasicView.prototype.setGridHeight = function (height, isAuto) {
8766
+        if (isAuto) {
8767
+            util_1.undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
8768
         }
8769
         else {
8770
-            classes.push('fc-' + util_1.dayIDs[date.day()]);
8771
-            if (view.isDateInOtherMonth(date, this.dateProfile)) {
8772
-                classes.push('fc-other-month');
8773
-            }
8774
-            today = view.calendar.getNow();
8775
-            if (date.isSame(today, 'day')) {
8776
-                classes.push('fc-today');
8777
-                if (noThemeHighlight !== true) {
8778
-                    classes.push(view.calendar.theme.getClass('today'));
8779
-                }
8780
-            }
8781
-            else if (date < today) {
8782
-                classes.push('fc-past');
8783
-            }
8784
-            else {
8785
-                classes.push('fc-future');
8786
-            }
8787
+            util_1.distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
8788
         }
8789
-        return classes;
8790
     };
8791
-    // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
8792
-    // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
8793
-    // The timezones of the dates within `range` will be respected.
8794
-    DateComponent.prototype.formatRange = function (range, isAllDay, formatStr, separator) {
8795
-        var end = range.end;
8796
-        if (isAllDay) {
8797
-            end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
8798
-        }
8799
-        return date_formatting_1.formatRange(range.start, end, formatStr, separator, this.isRTL);
8800
+    /* Scroll
8801
+    ------------------------------------------------------------------------------------------------------------------*/
8802
+    BasicView.prototype.computeInitialDateScroll = function () {
8803
+        return { top: 0 };
8804
     };
8805
-    // Compute the number of the give units in the "current" range.
8806
-    // Will return a floating-point number. Won't round.
8807
-    DateComponent.prototype.currentRangeAs = function (unit) {
8808
-        return this._getDateProfile().currentUnzonedRange.as(unit);
8809
+    BasicView.prototype.queryDateScroll = function () {
8810
+        return { top: this.scroller.getScrollTop() };
8811
     };
8812
-    // Returns the date range of the full days the given range visually appears to occupy.
8813
-    // Returns a plain object with start/end, NOT an UnzonedRange!
8814
-    DateComponent.prototype.computeDayRange = function (unzonedRange) {
8815
-        var calendar = this._getCalendar();
8816
-        var startDay = calendar.msToUtcMoment(unzonedRange.startMs, true); // the beginning of the day the range starts
8817
-        var end = calendar.msToUtcMoment(unzonedRange.endMs);
8818
-        var endTimeMS = +end.time(); // # of milliseconds into `endDay`
8819
-        var endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
8820
-        // If the end time is actually inclusively part of the next day and is equal to or
8821
-        // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
8822
-        // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
8823
-        if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
8824
-            endDay.add(1, 'days');
8825
-        }
8826
-        // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
8827
-        if (endDay <= startDay) {
8828
-            endDay = startDay.clone().add(1, 'days');
8829
+    BasicView.prototype.applyDateScroll = function (scroll) {
8830
+        if (scroll.top !== undefined) {
8831
+            this.scroller.setScrollTop(scroll.top);
8832
         }
8833
-        return { start: startDay, end: endDay };
8834
-    };
8835
-    // Does the given range visually appear to occupy more than one day?
8836
-    DateComponent.prototype.isMultiDayRange = function (unzonedRange) {
8837
-        var dayRange = this.computeDayRange(unzonedRange);
8838
-        return dayRange.end.diff(dayRange.start, 'days') > 1;
8839
     };
8840
-    DateComponent.guid = 0; // TODO: better system for this?
8841
-    return DateComponent;
8842
-}(Component_1.default));
8843
-exports.default = DateComponent;
8844
-// legacy
8845
-function convertEventsPayloadToLegacyArray(eventsPayload) {
8846
-    var eventDefId;
8847
-    var eventInstances;
8848
-    var legacyEvents = [];
8849
-    var i;
8850
-    for (eventDefId in eventsPayload) {
8851
-        eventInstances = eventsPayload[eventDefId].eventInstances;
8852
-        for (i = 0; i < eventInstances.length; i++) {
8853
-            legacyEvents.push(eventInstances[i].toLegacy());
8854
+    return BasicView;
8855
+}(View_1.default));
8856
+exports.default = BasicView;
8857
+BasicView.prototype.dateProfileGeneratorClass = BasicViewDateProfileGenerator_1.default;
8858
+BasicView.prototype.dayGridClass = DayGrid_1.default;
8859
+// customize the rendering behavior of BasicView's dayGrid
8860
+function makeDayGridSubclass(SuperClass) {
8861
+    return /** @class */ (function (_super) {
8862
+        tslib_1.__extends(SubClass, _super);
8863
+        function SubClass() {
8864
+            var _this = _super !== null && _super.apply(this, arguments) || this;
8865
+            _this.colWeekNumbersVisible = false; // display week numbers along the side?
8866
+            return _this;
8867
         }
8868
-    }
8869
-    return legacyEvents;
8870
+        // Generates the HTML that will go before the day-of week header cells
8871
+        SubClass.prototype.renderHeadIntroHtml = function () {
8872
+            var view = this.view;
8873
+            if (this.colWeekNumbersVisible) {
8874
+                return '' +
8875
+                    '<th class="fc-week-number ' + view.calendar.theme.getClass('widgetHeader') + '" ' + view.weekNumberStyleAttr() + '>' +
8876
+                    '<span>' + // needed for matchCellWidths
8877
+                    util_1.htmlEscape(this.opt('weekNumberTitle')) +
8878
+                    '</span>' +
8879
+                    '</th>';
8880
+            }
8881
+            return '';
8882
+        };
8883
+        // Generates the HTML that will go before content-skeleton cells that display the day/week numbers
8884
+        SubClass.prototype.renderNumberIntroHtml = function (row) {
8885
+            var view = this.view;
8886
+            var weekStart = this.getCellDate(row, 0);
8887
+            if (this.colWeekNumbersVisible) {
8888
+                return '' +
8889
+                    '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' +
8890
+                    view.buildGotoAnchorHtml(// aside from link, important for matchCellWidths
8891
+                    { date: weekStart, type: 'week', forceOff: this.colCnt === 1 }, weekStart.format('w') // inner HTML
8892
+                    ) +
8893
+                    '</td>';
8894
+            }
8895
+            return '';
8896
+        };
8897
+        // Generates the HTML that goes before the day bg cells for each day-row
8898
+        SubClass.prototype.renderBgIntroHtml = function () {
8899
+            var view = this.view;
8900
+            if (this.colWeekNumbersVisible) {
8901
+                return '<td class="fc-week-number ' + view.calendar.theme.getClass('widgetContent') + '" ' +
8902
+                    view.weekNumberStyleAttr() + '></td>';
8903
+            }
8904
+            return '';
8905
+        };
8906
+        // Generates the HTML that goes before every other type of row generated by DayGrid.
8907
+        // Affects helper-skeleton and highlight-skeleton rows.
8908
+        SubClass.prototype.renderIntroHtml = function () {
8909
+            var view = this.view;
8910
+            if (this.colWeekNumbersVisible) {
8911
+                return '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '></td>';
8912
+            }
8913
+            return '';
8914
+        };
8915
+        SubClass.prototype.getIsNumbersVisible = function () {
8916
+            return DayGrid_1.default.prototype.getIsNumbersVisible.apply(this, arguments) || this.colWeekNumbersVisible;
8917
+        };
8918
+        return SubClass;
8919
+    }(SuperClass));
8920
 }
8921
 
8922
 
8923
 /***/ }),
8924
-/* 220 */
8925
+/* 68 */
8926
+/***/ (function(module, exports, __webpack_require__) {
8927
+
8928
+Object.defineProperty(exports, "__esModule", { value: true });
8929
+var tslib_1 = __webpack_require__(2);
8930
+var UnzonedRange_1 = __webpack_require__(5);
8931
+var DateProfileGenerator_1 = __webpack_require__(55);
8932
+var BasicViewDateProfileGenerator = /** @class */ (function (_super) {
8933
+    tslib_1.__extends(BasicViewDateProfileGenerator, _super);
8934
+    function BasicViewDateProfileGenerator() {
8935
+        return _super !== null && _super.apply(this, arguments) || this;
8936
+    }
8937
+    // Computes the date range that will be rendered.
8938
+    BasicViewDateProfileGenerator.prototype.buildRenderRange = function (currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
8939
+        var renderUnzonedRange = _super.prototype.buildRenderRange.call(this, currentUnzonedRange, currentRangeUnit, isRangeAllDay); // an UnzonedRange
8940
+        var start = this.msToUtcMoment(renderUnzonedRange.startMs, isRangeAllDay);
8941
+        var end = this.msToUtcMoment(renderUnzonedRange.endMs, isRangeAllDay);
8942
+        // year and month views should be aligned with weeks. this is already done for week
8943
+        if (/^(year|month)$/.test(currentRangeUnit)) {
8944
+            start.startOf('week');
8945
+            // make end-of-week if not already
8946
+            if (end.weekday()) {
8947
+                end.add(1, 'week').startOf('week'); // exclusively move backwards
8948
+            }
8949
+        }
8950
+        return new UnzonedRange_1.default(start, end);
8951
+    };
8952
+    return BasicViewDateProfileGenerator;
8953
+}(DateProfileGenerator_1.default));
8954
+exports.default = BasicViewDateProfileGenerator;
8955
+
8956
+
8957
+/***/ }),
8958
+/* 69 */,
8959
+/* 70 */,
8960
+/* 71 */,
8961
+/* 72 */,
8962
+/* 73 */,
8963
+/* 74 */,
8964
+/* 75 */,
8965
+/* 76 */,
8966
+/* 77 */,
8967
+/* 78 */,
8968
+/* 79 */,
8969
+/* 80 */,
8970
+/* 81 */,
8971
+/* 82 */,
8972
+/* 83 */,
8973
+/* 84 */,
8974
+/* 85 */,
8975
+/* 86 */,
8976
+/* 87 */,
8977
+/* 88 */,
8978
+/* 89 */,
8979
+/* 90 */,
8980
+/* 91 */,
8981
+/* 92 */,
8982
+/* 93 */,
8983
+/* 94 */,
8984
+/* 95 */,
8985
+/* 96 */,
8986
+/* 97 */,
8987
+/* 98 */,
8988
+/* 99 */,
8989
+/* 100 */,
8990
+/* 101 */,
8991
+/* 102 */,
8992
+/* 103 */,
8993
+/* 104 */,
8994
+/* 105 */,
8995
+/* 106 */,
8996
+/* 107 */,
8997
+/* 108 */,
8998
+/* 109 */,
8999
+/* 110 */,
9000
+/* 111 */,
9001
+/* 112 */,
9002
+/* 113 */,
9003
+/* 114 */,
9004
+/* 115 */,
9005
+/* 116 */,
9006
+/* 117 */,
9007
+/* 118 */,
9008
+/* 119 */,
9009
+/* 120 */,
9010
+/* 121 */,
9011
+/* 122 */,
9012
+/* 123 */,
9013
+/* 124 */,
9014
+/* 125 */,
9015
+/* 126 */,
9016
+/* 127 */,
9017
+/* 128 */,
9018
+/* 129 */,
9019
+/* 130 */,
9020
+/* 131 */,
9021
+/* 132 */,
9022
+/* 133 */,
9023
+/* 134 */,
9024
+/* 135 */,
9025
+/* 136 */,
9026
+/* 137 */,
9027
+/* 138 */,
9028
+/* 139 */,
9029
+/* 140 */,
9030
+/* 141 */,
9031
+/* 142 */,
9032
+/* 143 */,
9033
+/* 144 */,
9034
+/* 145 */,
9035
+/* 146 */,
9036
+/* 147 */,
9037
+/* 148 */,
9038
+/* 149 */,
9039
+/* 150 */,
9040
+/* 151 */,
9041
+/* 152 */,
9042
+/* 153 */,
9043
+/* 154 */,
9044
+/* 155 */,
9045
+/* 156 */,
9046
+/* 157 */,
9047
+/* 158 */,
9048
+/* 159 */,
9049
+/* 160 */,
9050
+/* 161 */,
9051
+/* 162 */,
9052
+/* 163 */,
9053
+/* 164 */,
9054
+/* 165 */,
9055
+/* 166 */,
9056
+/* 167 */,
9057
+/* 168 */,
9058
+/* 169 */,
9059
+/* 170 */,
9060
+/* 171 */,
9061
+/* 172 */,
9062
+/* 173 */,
9063
+/* 174 */,
9064
+/* 175 */,
9065
+/* 176 */,
9066
+/* 177 */,
9067
+/* 178 */,
9068
+/* 179 */,
9069
+/* 180 */,
9070
+/* 181 */,
9071
+/* 182 */,
9072
+/* 183 */,
9073
+/* 184 */,
9074
+/* 185 */,
9075
+/* 186 */,
9076
+/* 187 */,
9077
+/* 188 */,
9078
+/* 189 */,
9079
+/* 190 */,
9080
+/* 191 */,
9081
+/* 192 */,
9082
+/* 193 */,
9083
+/* 194 */,
9084
+/* 195 */,
9085
+/* 196 */,
9086
+/* 197 */,
9087
+/* 198 */,
9088
+/* 199 */,
9089
+/* 200 */,
9090
+/* 201 */,
9091
+/* 202 */,
9092
+/* 203 */,
9093
+/* 204 */,
9094
+/* 205 */,
9095
+/* 206 */,
9096
+/* 207 */,
9097
+/* 208 */,
9098
+/* 209 */,
9099
+/* 210 */,
9100
+/* 211 */,
9101
+/* 212 */,
9102
+/* 213 */,
9103
+/* 214 */,
9104
+/* 215 */,
9105
+/* 216 */,
9106
+/* 217 */
9107
 /***/ (function(module, exports, __webpack_require__) {
9108
 
9109
 Object.defineProperty(exports, "__esModule", { value: true });
9110
-var $ = __webpack_require__(3);
9111
-var moment = __webpack_require__(0);
9112
-var util_1 = __webpack_require__(4);
9113
-var options_1 = __webpack_require__(32);
9114
-var Iterator_1 = __webpack_require__(238);
9115
-var GlobalEmitter_1 = __webpack_require__(21);
9116
-var EmitterMixin_1 = __webpack_require__(11);
9117
-var ListenerMixin_1 = __webpack_require__(7);
9118
-var Toolbar_1 = __webpack_require__(239);
9119
-var OptionsManager_1 = __webpack_require__(240);
9120
-var ViewSpecManager_1 = __webpack_require__(241);
9121
-var Constraints_1 = __webpack_require__(207);
9122
-var locale_1 = __webpack_require__(31);
9123
-var moment_ext_1 = __webpack_require__(10);
9124
 var UnzonedRange_1 = __webpack_require__(5);
9125
 var ComponentFootprint_1 = __webpack_require__(12);
9126
-var EventDateProfile_1 = __webpack_require__(17);
9127
-var EventManager_1 = __webpack_require__(242);
9128
-var BusinessHourGenerator_1 = __webpack_require__(212);
9129
-var EventSourceParser_1 = __webpack_require__(38);
9130
-var EventDefParser_1 = __webpack_require__(49);
9131
-var SingleEventDef_1 = __webpack_require__(13);
9132
-var EventDefMutation_1 = __webpack_require__(37);
9133
+var EventDefParser_1 = __webpack_require__(36);
9134
 var EventSource_1 = __webpack_require__(6);
9135
-var ThemeRegistry_1 = __webpack_require__(51);
9136
-var Calendar = /** @class */ (function () {
9137
-    function Calendar(el, overrides) {
9138
-        this.loadingLevel = 0; // number of simultaneous loading tasks
9139
-        this.ignoreUpdateViewSize = 0;
9140
-        this.freezeContentHeightDepth = 0;
9141
-        // declare the current calendar instance relies on GlobalEmitter. needed for garbage collection.
9142
-        // unneeded() is called in destroy.
9143
-        GlobalEmitter_1.default.needed();
9144
-        this.el = el;
9145
-        this.viewsByType = {};
9146
-        this.optionsManager = new OptionsManager_1.default(this, overrides);
9147
-        this.viewSpecManager = new ViewSpecManager_1.default(this.optionsManager, this);
9148
-        this.initMomentInternals(); // needs to happen after options hash initialized
9149
-        this.initCurrentDate();
9150
-        this.initEventManager();
9151
-        this.constraints = new Constraints_1.default(this.eventManager, this);
9152
-        this.constructed();
9153
+var util_1 = __webpack_require__(19);
9154
+var Constraints = /** @class */ (function () {
9155
+    function Constraints(eventManager, _calendar) {
9156
+        this.eventManager = eventManager;
9157
+        this._calendar = _calendar;
9158
     }
9159
-    Calendar.prototype.constructed = function () {
9160
-        // useful for monkeypatching. used?
9161
-    };
9162
-    Calendar.prototype.getView = function () {
9163
-        return this.view;
9164
+    Constraints.prototype.opt = function (name) {
9165
+        return this._calendar.opt(name);
9166
     };
9167
-    Calendar.prototype.publiclyTrigger = function (name, triggerInfo) {
9168
-        var optHandler = this.opt(name);
9169
-        var context;
9170
-        var args;
9171
-        if ($.isPlainObject(triggerInfo)) {
9172
-            context = triggerInfo.context;
9173
-            args = triggerInfo.args;
9174
-        }
9175
-        else if ($.isArray(triggerInfo)) {
9176
-            args = triggerInfo;
9177
-        }
9178
-        if (context == null) {
9179
-            context = this.el[0]; // fallback context
9180
-        }
9181
-        if (!args) {
9182
-            args = [];
9183
+    /*
9184
+    determines if eventInstanceGroup is allowed,
9185
+    in relation to other EVENTS and business hours.
9186
+    */
9187
+    Constraints.prototype.isEventInstanceGroupAllowed = function (eventInstanceGroup) {
9188
+        var eventDef = eventInstanceGroup.getEventDef();
9189
+        var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
9190
+        var i;
9191
+        var peerEventInstances = this.getPeerEventInstances(eventDef);
9192
+        var peerEventRanges = peerEventInstances.map(util_1.eventInstanceToEventRange);
9193
+        var peerEventFootprints = this.eventRangesToEventFootprints(peerEventRanges);
9194
+        var constraintVal = eventDef.getConstraint();
9195
+        var overlapVal = eventDef.getOverlap();
9196
+        var eventAllowFunc = this.opt('eventAllow');
9197
+        for (i = 0; i < eventFootprints.length; i++) {
9198
+            if (!this.isFootprintAllowed(eventFootprints[i].componentFootprint, peerEventFootprints, constraintVal, overlapVal, eventFootprints[i].eventInstance)) {
9199
+                return false;
9200
+            }
9201
         }
9202
-        this.triggerWith(name, context, args); // Emitter's method
9203
-        if (optHandler) {
9204
-            return optHandler.apply(context, args);
9205
+        if (eventAllowFunc) {
9206
+            for (i = 0; i < eventFootprints.length; i++) {
9207
+                if (eventAllowFunc(eventFootprints[i].componentFootprint.toLegacy(this._calendar), eventFootprints[i].getEventLegacy()) === false) {
9208
+                    return false;
9209
+                }
9210
+            }
9211
         }
9212
+        return true;
9213
     };
9214
-    Calendar.prototype.hasPublicHandlers = function (name) {
9215
-        return this.hasHandlers(name) ||
9216
-            this.opt(name); // handler specified in options
9217
+    Constraints.prototype.getPeerEventInstances = function (eventDef) {
9218
+        return this.eventManager.getEventInstancesWithoutId(eventDef.id);
9219
     };
9220
-    // Options Public API
9221
-    // -----------------------------------------------------------------------------------------------------------------
9222
-    // public getter/setter
9223
-    Calendar.prototype.option = function (name, value) {
9224
-        var newOptionHash;
9225
-        if (typeof name === 'string') {
9226
-            if (value === undefined) {
9227
-                return this.optionsManager.get(name);
9228
+    Constraints.prototype.isSelectionFootprintAllowed = function (componentFootprint) {
9229
+        var peerEventInstances = this.eventManager.getEventInstances();
9230
+        var peerEventRanges = peerEventInstances.map(util_1.eventInstanceToEventRange);
9231
+        var peerEventFootprints = this.eventRangesToEventFootprints(peerEventRanges);
9232
+        var selectAllowFunc;
9233
+        if (this.isFootprintAllowed(componentFootprint, peerEventFootprints, this.opt('selectConstraint'), this.opt('selectOverlap'))) {
9234
+            selectAllowFunc = this.opt('selectAllow');
9235
+            if (selectAllowFunc) {
9236
+                return selectAllowFunc(componentFootprint.toLegacy(this._calendar)) !== false;
9237
             }
9238
             else {
9239
-                newOptionHash = {};
9240
-                newOptionHash[name] = value;
9241
-                this.optionsManager.add(newOptionHash);
9242
+                return true;
9243
             }
9244
         }
9245
-        else if (typeof name === 'object') {
9246
-            this.optionsManager.add(name);
9247
+        return false;
9248
+    };
9249
+    Constraints.prototype.isFootprintAllowed = function (componentFootprint, peerEventFootprints, constraintVal, overlapVal, subjectEventInstance // optional
9250
+    ) {
9251
+        var constraintFootprints; // ComponentFootprint[]
9252
+        var overlapEventFootprints; // EventFootprint[]
9253
+        if (constraintVal != null) {
9254
+            constraintFootprints = this.constraintValToFootprints(constraintVal, componentFootprint.isAllDay);
9255
+            if (!this.isFootprintWithinConstraints(componentFootprint, constraintFootprints)) {
9256
+                return false;
9257
+            }
9258
+        }
9259
+        overlapEventFootprints = this.collectOverlapEventFootprints(peerEventFootprints, componentFootprint);
9260
+        if (overlapVal === false) {
9261
+            if (overlapEventFootprints.length) {
9262
+                return false;
9263
+            }
9264
+        }
9265
+        else if (typeof overlapVal === 'function') {
9266
+            if (!isOverlapsAllowedByFunc(overlapEventFootprints, overlapVal, subjectEventInstance)) {
9267
+                return false;
9268
+            }
9269
+        }
9270
+        if (subjectEventInstance) {
9271
+            if (!isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInstance)) {
9272
+                return false;
9273
+            }
9274
         }
9275
+        return true;
9276
     };
9277
-    // private getter
9278
-    Calendar.prototype.opt = function (name) {
9279
-        return this.optionsManager.get(name);
9280
-    };
9281
-    // View
9282
-    // -----------------------------------------------------------------------------------------------------------------
9283
-    // Given a view name for a custom view or a standard view, creates a ready-to-go View object
9284
-    Calendar.prototype.instantiateView = function (viewType) {
9285
-        var spec = this.viewSpecManager.getViewSpec(viewType);
9286
-        if (!spec) {
9287
-            throw new Error("View type \"" + viewType + "\" is not valid");
9288
+    // Constraint
9289
+    // ------------------------------------------------------------------------------------------------
9290
+    Constraints.prototype.isFootprintWithinConstraints = function (componentFootprint, constraintFootprints) {
9291
+        var i;
9292
+        for (i = 0; i < constraintFootprints.length; i++) {
9293
+            if (this.footprintContainsFootprint(constraintFootprints[i], componentFootprint)) {
9294
+                return true;
9295
+            }
9296
         }
9297
-        return new spec['class'](this, spec);
9298
-    };
9299
-    // Returns a boolean about whether the view is okay to instantiate at some point
9300
-    Calendar.prototype.isValidViewType = function (viewType) {
9301
-        return Boolean(this.viewSpecManager.getViewSpec(viewType));
9302
+        return false;
9303
     };
9304
-    Calendar.prototype.changeView = function (viewName, dateOrRange) {
9305
-        if (dateOrRange) {
9306
-            if (dateOrRange.start && dateOrRange.end) {
9307
-                this.optionsManager.recordOverrides({
9308
-                    visibleRange: dateOrRange
9309
-                });
9310
+    Constraints.prototype.constraintValToFootprints = function (constraintVal, isAllDay) {
9311
+        var eventInstances;
9312
+        if (constraintVal === 'businessHours') {
9313
+            return this.buildCurrentBusinessFootprints(isAllDay);
9314
+        }
9315
+        else if (typeof constraintVal === 'object') {
9316
+            eventInstances = this.parseEventDefToInstances(constraintVal); // handles recurring events
9317
+            if (!eventInstances) { // invalid input. fallback to parsing footprint directly
9318
+                return this.parseFootprints(constraintVal);
9319
             }
9320
             else {
9321
-                this.currentDate = this.moment(dateOrRange).stripZone(); // just like gotoDate
9322
+                return this.eventInstancesToFootprints(eventInstances);
9323
             }
9324
         }
9325
-        this.renderView(viewName);
9326
-    };
9327
-    // Forces navigation to a view for the given date.
9328
-    // `viewType` can be a specific view name or a generic one like "week" or "day".
9329
-    Calendar.prototype.zoomTo = function (newDate, viewType) {
9330
-        var spec;
9331
-        viewType = viewType || 'day'; // day is default zoom
9332
-        spec = this.viewSpecManager.getViewSpec(viewType) ||
9333
-            this.viewSpecManager.getUnitViewSpec(viewType);
9334
-        this.currentDate = newDate.clone();
9335
-        this.renderView(spec ? spec.type : null);
9336
+        else if (constraintVal != null) { // an ID
9337
+            eventInstances = this.eventManager.getEventInstancesWithId(constraintVal);
9338
+            return this.eventInstancesToFootprints(eventInstances);
9339
+        }
9340
     };
9341
-    // Current Date
9342
-    // -----------------------------------------------------------------------------------------------------------------
9343
-    Calendar.prototype.initCurrentDate = function () {
9344
-        var defaultDateInput = this.opt('defaultDate');
9345
-        // compute the initial ambig-timezone date
9346
-        if (defaultDateInput != null) {
9347
-            this.currentDate = this.moment(defaultDateInput).stripZone();
9348
+    // returns ComponentFootprint[]
9349
+    // uses current view's range
9350
+    Constraints.prototype.buildCurrentBusinessFootprints = function (isAllDay) {
9351
+        var view = this._calendar.view;
9352
+        var businessHourGenerator = view.get('businessHourGenerator');
9353
+        var unzonedRange = view.dateProfile.activeUnzonedRange;
9354
+        var eventInstanceGroup = businessHourGenerator.buildEventInstanceGroup(isAllDay, unzonedRange);
9355
+        if (eventInstanceGroup) {
9356
+            return this.eventInstancesToFootprints(eventInstanceGroup.eventInstances);
9357
         }
9358
         else {
9359
-            this.currentDate = this.getNow(); // getNow already returns unzoned
9360
+            return [];
9361
         }
9362
     };
9363
-    Calendar.prototype.prev = function () {
9364
-        var view = this.view;
9365
-        var prevInfo = view.dateProfileGenerator.buildPrev(view.get('dateProfile'));
9366
-        if (prevInfo.isValid) {
9367
-            this.currentDate = prevInfo.date;
9368
-            this.renderView();
9369
-        }
9370
+    // conversion util
9371
+    Constraints.prototype.eventInstancesToFootprints = function (eventInstances) {
9372
+        var eventRanges = eventInstances.map(util_1.eventInstanceToEventRange);
9373
+        var eventFootprints = this.eventRangesToEventFootprints(eventRanges);
9374
+        return eventFootprints.map(util_1.eventFootprintToComponentFootprint);
9375
     };
9376
-    Calendar.prototype.next = function () {
9377
-        var view = this.view;
9378
-        var nextInfo = view.dateProfileGenerator.buildNext(view.get('dateProfile'));
9379
-        if (nextInfo.isValid) {
9380
-            this.currentDate = nextInfo.date;
9381
-            this.renderView();
9382
+    // Overlap
9383
+    // ------------------------------------------------------------------------------------------------
9384
+    Constraints.prototype.collectOverlapEventFootprints = function (peerEventFootprints, targetFootprint) {
9385
+        var overlapEventFootprints = [];
9386
+        var i;
9387
+        for (i = 0; i < peerEventFootprints.length; i++) {
9388
+            if (this.footprintsIntersect(targetFootprint, peerEventFootprints[i].componentFootprint)) {
9389
+                overlapEventFootprints.push(peerEventFootprints[i]);
9390
+            }
9391
         }
9392
+        return overlapEventFootprints;
9393
     };
9394
-    Calendar.prototype.prevYear = function () {
9395
-        this.currentDate.add(-1, 'years');
9396
-        this.renderView();
9397
+    // Conversion: eventDefs -> eventInstances -> eventRanges -> eventFootprints -> componentFootprints
9398
+    // ------------------------------------------------------------------------------------------------
9399
+    // NOTE: this might seem like repetitive code with the Grid class, however, this code is related to
9400
+    // constraints whereas the Grid code is related to rendering. Each approach might want to convert
9401
+    // eventRanges -> eventFootprints in a different way. Regardless, there are opportunities to make
9402
+    // this more DRY.
9403
+    /*
9404
+    Returns false on invalid input.
9405
+    */
9406
+    Constraints.prototype.parseEventDefToInstances = function (eventInput) {
9407
+        var eventManager = this.eventManager;
9408
+        var eventDef = EventDefParser_1.default.parse(eventInput, new EventSource_1.default(this._calendar));
9409
+        if (!eventDef) { // invalid
9410
+            return false;
9411
+        }
9412
+        return eventDef.buildInstances(eventManager.currentPeriod.unzonedRange);
9413
     };
9414
-    Calendar.prototype.nextYear = function () {
9415
-        this.currentDate.add(1, 'years');
9416
-        this.renderView();
9417
+    Constraints.prototype.eventRangesToEventFootprints = function (eventRanges) {
9418
+        var i;
9419
+        var eventFootprints = [];
9420
+        for (i = 0; i < eventRanges.length; i++) {
9421
+            eventFootprints.push.apply(// footprints
9422
+            eventFootprints, this.eventRangeToEventFootprints(eventRanges[i]));
9423
+        }
9424
+        return eventFootprints;
9425
     };
9426
-    Calendar.prototype.today = function () {
9427
-        this.currentDate = this.getNow(); // should deny like prev/next?
9428
-        this.renderView();
9429
+    Constraints.prototype.eventRangeToEventFootprints = function (eventRange) {
9430
+        return [util_1.eventRangeToEventFootprint(eventRange)];
9431
     };
9432
-    Calendar.prototype.gotoDate = function (zonedDateInput) {
9433
-        this.currentDate = this.moment(zonedDateInput).stripZone();
9434
-        this.renderView();
9435
+    /*
9436
+    Parses footprints directly.
9437
+    Very similar to EventDateProfile::parse :(
9438
+    */
9439
+    Constraints.prototype.parseFootprints = function (rawInput) {
9440
+        var start;
9441
+        var end;
9442
+        if (rawInput.start) {
9443
+            start = this._calendar.moment(rawInput.start);
9444
+            if (!start.isValid()) {
9445
+                start = null;
9446
+            }
9447
+        }
9448
+        if (rawInput.end) {
9449
+            end = this._calendar.moment(rawInput.end);
9450
+            if (!end.isValid()) {
9451
+                end = null;
9452
+            }
9453
+        }
9454
+        return [
9455
+            new ComponentFootprint_1.default(new UnzonedRange_1.default(start, end), (start && !start.hasTime()) || (end && !end.hasTime()) // isAllDay
9456
+            )
9457
+        ];
9458
     };
9459
-    Calendar.prototype.incrementDate = function (delta) {
9460
-        this.currentDate.add(moment.duration(delta));
9461
-        this.renderView();
9462
+    // Footprint Utils
9463
+    // ----------------------------------------------------------------------------------------
9464
+    Constraints.prototype.footprintContainsFootprint = function (outerFootprint, innerFootprint) {
9465
+        return outerFootprint.unzonedRange.containsRange(innerFootprint.unzonedRange);
9466
     };
9467
-    // for external API
9468
-    Calendar.prototype.getDate = function () {
9469
-        return this.applyTimezone(this.currentDate); // infuse the calendar's timezone
9470
+    Constraints.prototype.footprintsIntersect = function (footprint0, footprint1) {
9471
+        return footprint0.unzonedRange.intersectsWith(footprint1.unzonedRange);
9472
     };
9473
-    // Loading Triggering
9474
-    // -----------------------------------------------------------------------------------------------------------------
9475
-    // Should be called when any type of async data fetching begins
9476
-    Calendar.prototype.pushLoading = function () {
9477
-        if (!(this.loadingLevel++)) {
9478
-            this.publiclyTrigger('loading', [true, this.view]);
9479
+    return Constraints;
9480
+}());
9481
+exports.default = Constraints;
9482
+// optional subjectEventInstance
9483
+function isOverlapsAllowedByFunc(overlapEventFootprints, overlapFunc, subjectEventInstance) {
9484
+    var i;
9485
+    for (i = 0; i < overlapEventFootprints.length; i++) {
9486
+        if (!overlapFunc(overlapEventFootprints[i].eventInstance.toLegacy(), subjectEventInstance ? subjectEventInstance.toLegacy() : null)) {
9487
+            return false;
9488
+        }
9489
+    }
9490
+    return true;
9491
+}
9492
+function isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInstance) {
9493
+    var subjectLegacyInstance = subjectEventInstance.toLegacy();
9494
+    var i;
9495
+    var overlapEventInstance;
9496
+    var overlapEventDef;
9497
+    var overlapVal;
9498
+    for (i = 0; i < overlapEventFootprints.length; i++) {
9499
+        overlapEventInstance = overlapEventFootprints[i].eventInstance;
9500
+        overlapEventDef = overlapEventInstance.def;
9501
+        // don't need to pass in calendar, because don't want to consider global eventOverlap property,
9502
+        // because we already considered that earlier in the process.
9503
+        overlapVal = overlapEventDef.getOverlap();
9504
+        if (overlapVal === false) {
9505
+            return false;
9506
+        }
9507
+        else if (typeof overlapVal === 'function') {
9508
+            if (!overlapVal(overlapEventInstance.toLegacy(), subjectLegacyInstance)) {
9509
+                return false;
9510
+            }
9511
+        }
9512
+    }
9513
+    return true;
9514
+}
9515
+
9516
+
9517
+/***/ }),
9518
+/* 218 */
9519
+/***/ (function(module, exports, __webpack_require__) {
9520
+
9521
+Object.defineProperty(exports, "__esModule", { value: true });
9522
+var $ = __webpack_require__(3);
9523
+var util_1 = __webpack_require__(19);
9524
+var EventInstanceGroup_1 = __webpack_require__(20);
9525
+var RecurringEventDef_1 = __webpack_require__(54);
9526
+var EventSource_1 = __webpack_require__(6);
9527
+var BUSINESS_HOUR_EVENT_DEFAULTS = {
9528
+    start: '09:00',
9529
+    end: '17:00',
9530
+    dow: [1, 2, 3, 4, 5],
9531
+    rendering: 'inverse-background'
9532
+    // classNames are defined in businessHoursSegClasses
9533
+};
9534
+var BusinessHourGenerator = /** @class */ (function () {
9535
+    function BusinessHourGenerator(rawComplexDef, calendar) {
9536
+        this.rawComplexDef = rawComplexDef;
9537
+        this.calendar = calendar;
9538
+    }
9539
+    BusinessHourGenerator.prototype.buildEventInstanceGroup = function (isAllDay, unzonedRange) {
9540
+        var eventDefs = this.buildEventDefs(isAllDay);
9541
+        var eventInstanceGroup;
9542
+        if (eventDefs.length) {
9543
+            eventInstanceGroup = new EventInstanceGroup_1.default(util_1.eventDefsToEventInstances(eventDefs, unzonedRange));
9544
+            // so that inverse-background rendering can happen even when no eventRanges in view
9545
+            eventInstanceGroup.explicitEventDef = eventDefs[0];
9546
+            return eventInstanceGroup;
9547
         }
9548
     };
9549
-    // Should be called when any type of async data fetching completes
9550
-    Calendar.prototype.popLoading = function () {
9551
-        if (!(--this.loadingLevel)) {
9552
-            this.publiclyTrigger('loading', [false, this.view]);
9553
+    BusinessHourGenerator.prototype.buildEventDefs = function (isAllDay) {
9554
+        var rawComplexDef = this.rawComplexDef;
9555
+        var rawDefs = [];
9556
+        var requireDow = false;
9557
+        var i;
9558
+        var defs = [];
9559
+        if (rawComplexDef === true) {
9560
+            rawDefs = [{}]; // will get BUSINESS_HOUR_EVENT_DEFAULTS verbatim
9561
         }
9562
-    };
9563
-    // High-level Rendering
9564
-    // -----------------------------------------------------------------------------------
9565
-    Calendar.prototype.render = function () {
9566
-        if (!this.contentEl) {
9567
-            this.initialRender();
9568
+        else if ($.isPlainObject(rawComplexDef)) {
9569
+            rawDefs = [rawComplexDef];
9570
         }
9571
-        else if (this.elementVisible()) {
9572
-            // mainly for the public API
9573
-            this.calcSize();
9574
-            this.updateViewSize();
9575
+        else if ($.isArray(rawComplexDef)) {
9576
+            rawDefs = rawComplexDef;
9577
+            requireDow = true; // every sub-definition NEEDS a day-of-week
9578
         }
9579
-    };
9580
-    Calendar.prototype.initialRender = function () {
9581
-        var _this = this;
9582
-        var el = this.el;
9583
-        el.addClass('fc');
9584
-        // event delegation for nav links
9585
-        el.on('click.fc', 'a[data-goto]', function (ev) {
9586
-            var anchorEl = $(ev.currentTarget);
9587
-            var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON
9588
-            var date = _this.moment(gotoOptions.date);
9589
-            var viewType = gotoOptions.type;
9590
-            // property like "navLinkDayClick". might be a string or a function
9591
-            var customAction = _this.view.opt('navLink' + util_1.capitaliseFirstLetter(viewType) + 'Click');
9592
-            if (typeof customAction === 'function') {
9593
-                customAction(date, ev);
9594
-            }
9595
-            else {
9596
-                if (typeof customAction === 'string') {
9597
-                    viewType = customAction;
9598
-                }
9599
-                _this.zoomTo(date, viewType);
9600
-            }
9601
-        });
9602
-        // called immediately, and upon option change
9603
-        this.optionsManager.watch('settingTheme', ['?theme', '?themeSystem'], function (opts) {
9604
-            var themeClass = ThemeRegistry_1.getThemeSystemClass(opts.themeSystem || opts.theme);
9605
-            var theme = new themeClass(_this.optionsManager);
9606
-            var widgetClass = theme.getClass('widget');
9607
-            _this.theme = theme;
9608
-            if (widgetClass) {
9609
-                el.addClass(widgetClass);
9610
-            }
9611
-        }, function () {
9612
-            var widgetClass = _this.theme.getClass('widget');
9613
-            _this.theme = null;
9614
-            if (widgetClass) {
9615
-                el.removeClass(widgetClass);
9616
-            }
9617
-        });
9618
-        this.optionsManager.watch('settingBusinessHourGenerator', ['?businessHours'], function (deps) {
9619
-            _this.businessHourGenerator = new BusinessHourGenerator_1.default(deps.businessHours, _this);
9620
-            if (_this.view) {
9621
-                _this.view.set('businessHourGenerator', _this.businessHourGenerator);
9622
+        for (i = 0; i < rawDefs.length; i++) {
9623
+            if (!requireDow || rawDefs[i].dow) {
9624
+                defs.push(this.buildEventDef(isAllDay, rawDefs[i]));
9625
             }
9626
-        }, function () {
9627
-            _this.businessHourGenerator = null;
9628
-        });
9629
-        // called immediately, and upon option change.
9630
-        // HACK: locale often affects isRTL, so we explicitly listen to that too.
9631
-        this.optionsManager.watch('applyingDirClasses', ['?isRTL', '?locale'], function (opts) {
9632
-            el.toggleClass('fc-ltr', !opts.isRTL);
9633
-            el.toggleClass('fc-rtl', opts.isRTL);
9634
-        });
9635
-        this.contentEl = $("<div class='fc-view-container'/>").prependTo(el);
9636
-        this.initToolbars();
9637
-        this.renderHeader();
9638
-        this.renderFooter();
9639
-        this.renderView(this.opt('defaultView'));
9640
-        if (this.opt('handleWindowResize')) {
9641
-            $(window).resize(this.windowResizeProxy = util_1.debounce(// prevents rapid calls
9642
-            this.windowResize.bind(this), this.opt('windowResizeDelay')));
9643
         }
9644
+        return defs;
9645
     };
9646
-    Calendar.prototype.destroy = function () {
9647
-        if (this.view) {
9648
-            this.clearView();
9649
-        }
9650
-        this.toolbarsManager.proxyCall('removeElement');
9651
-        this.contentEl.remove();
9652
-        this.el.removeClass('fc fc-ltr fc-rtl');
9653
-        // removes theme-related root className
9654
-        this.optionsManager.unwatch('settingTheme');
9655
-        this.optionsManager.unwatch('settingBusinessHourGenerator');
9656
-        this.el.off('.fc'); // unbind nav link handlers
9657
-        if (this.windowResizeProxy) {
9658
-            $(window).unbind('resize', this.windowResizeProxy);
9659
-            this.windowResizeProxy = null;
9660
+    BusinessHourGenerator.prototype.buildEventDef = function (isAllDay, rawDef) {
9661
+        var fullRawDef = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, rawDef);
9662
+        if (isAllDay) {
9663
+            fullRawDef.start = null;
9664
+            fullRawDef.end = null;
9665
         }
9666
-        GlobalEmitter_1.default.unneeded();
9667
+        return RecurringEventDef_1.default.parse(fullRawDef, new EventSource_1.default(this.calendar) // dummy source
9668
+        );
9669
     };
9670
-    Calendar.prototype.elementVisible = function () {
9671
-        return this.el.is(':visible');
9672
+    return BusinessHourGenerator;
9673
+}());
9674
+exports.default = BusinessHourGenerator;
9675
+
9676
+
9677
+/***/ }),
9678
+/* 219 */
9679
+/***/ (function(module, exports, __webpack_require__) {
9680
+
9681
+Object.defineProperty(exports, "__esModule", { value: true });
9682
+var $ = __webpack_require__(3);
9683
+var util_1 = __webpack_require__(4);
9684
+var Promise_1 = __webpack_require__(21);
9685
+var EmitterMixin_1 = __webpack_require__(13);
9686
+var UnzonedRange_1 = __webpack_require__(5);
9687
+var EventInstanceGroup_1 = __webpack_require__(20);
9688
+var EventPeriod = /** @class */ (function () {
9689
+    function EventPeriod(start, end, timezone) {
9690
+        this.pendingCnt = 0;
9691
+        this.freezeDepth = 0;
9692
+        this.stuntedReleaseCnt = 0;
9693
+        this.releaseCnt = 0;
9694
+        this.start = start;
9695
+        this.end = end;
9696
+        this.timezone = timezone;
9697
+        this.unzonedRange = new UnzonedRange_1.default(start.clone().stripZone(), end.clone().stripZone());
9698
+        this.requestsByUid = {};
9699
+        this.eventDefsByUid = {};
9700
+        this.eventDefsById = {};
9701
+        this.eventInstanceGroupsById = {};
9702
+    }
9703
+    EventPeriod.prototype.isWithinRange = function (start, end) {
9704
+        // TODO: use a range util function?
9705
+        return !start.isBefore(this.start) && !end.isAfter(this.end);
9706
     };
9707
-    // Render Queue
9708
+    // Requesting and Purging
9709
     // -----------------------------------------------------------------------------------------------------------------
9710
-    Calendar.prototype.bindViewHandlers = function (view) {
9711
+    EventPeriod.prototype.requestSources = function (sources) {
9712
+        this.freeze();
9713
+        for (var i = 0; i < sources.length; i++) {
9714
+            this.requestSource(sources[i]);
9715
+        }
9716
+        this.thaw();
9717
+    };
9718
+    EventPeriod.prototype.requestSource = function (source) {
9719
         var _this = this;
9720
-        view.watch('titleForCalendar', ['title'], function (deps) {
9721
-            if (view === _this.view) {
9722
-                _this.setToolbarsTitle(deps.title);
9723
+        var request = { source: source, status: 'pending', eventDefs: null };
9724
+        this.requestsByUid[source.uid] = request;
9725
+        this.pendingCnt += 1;
9726
+        source.fetch(this.start, this.end, this.timezone).then(function (eventDefs) {
9727
+            if (request.status !== 'cancelled') {
9728
+                request.status = 'completed';
9729
+                request.eventDefs = eventDefs;
9730
+                _this.addEventDefs(eventDefs);
9731
+                _this.pendingCnt--;
9732
+                _this.tryRelease();
9733
             }
9734
-        });
9735
-        view.watch('dateProfileForCalendar', ['dateProfile'], function (deps) {
9736
-            if (view === _this.view) {
9737
-                _this.currentDate = deps.dateProfile.date; // might have been constrained by view dates
9738
-                _this.updateToolbarButtons(deps.dateProfile);
9739
+        }, function () {
9740
+            if (request.status !== 'cancelled') {
9741
+                request.status = 'failed';
9742
+                _this.pendingCnt--;
9743
+                _this.tryRelease();
9744
             }
9745
         });
9746
     };
9747
-    Calendar.prototype.unbindViewHandlers = function (view) {
9748
-        view.unwatch('titleForCalendar');
9749
-        view.unwatch('dateProfileForCalendar');
9750
-    };
9751
-    // View Rendering
9752
-    // -----------------------------------------------------------------------------------
9753
-    // Renders a view because of a date change, view-type change, or for the first time.
9754
-    // If not given a viewType, keep the current view but render different dates.
9755
-    // Accepts an optional scroll state to restore to.
9756
-    Calendar.prototype.renderView = function (viewType) {
9757
-        var oldView = this.view;
9758
-        var newView;
9759
-        this.freezeContentHeight();
9760
-        if (oldView && viewType && oldView.type !== viewType) {
9761
-            this.clearView();
9762
-        }
9763
-        // if viewType changed, or the view was never created, create a fresh view
9764
-        if (!this.view && viewType) {
9765
-            newView = this.view =
9766
-                this.viewsByType[viewType] ||
9767
-                    (this.viewsByType[viewType] = this.instantiateView(viewType));
9768
-            this.bindViewHandlers(newView);
9769
-            newView.startBatchRender(); // so that setElement+setDate rendering are joined
9770
-            newView.setElement($("<div class='fc-view fc-" + viewType + "-view' />").appendTo(this.contentEl));
9771
-            this.toolbarsManager.proxyCall('activateButton', viewType);
9772
-        }
9773
-        if (this.view) {
9774
-            // prevent unnecessary change firing
9775
-            if (this.view.get('businessHourGenerator') !== this.businessHourGenerator) {
9776
-                this.view.set('businessHourGenerator', this.businessHourGenerator);
9777
+    EventPeriod.prototype.purgeSource = function (source) {
9778
+        var request = this.requestsByUid[source.uid];
9779
+        if (request) {
9780
+            delete this.requestsByUid[source.uid];
9781
+            if (request.status === 'pending') {
9782
+                request.status = 'cancelled';
9783
+                this.pendingCnt--;
9784
+                this.tryRelease();
9785
             }
9786
-            this.view.setDate(this.currentDate);
9787
-            if (newView) {
9788
-                newView.stopBatchRender();
9789
+            else if (request.status === 'completed') {
9790
+                request.eventDefs.forEach(this.removeEventDef.bind(this));
9791
             }
9792
         }
9793
-        this.thawContentHeight();
9794
-    };
9795
-    // Unrenders the current view and reflects this change in the Header.
9796
-    // Unregsiters the `view`, but does not remove from viewByType hash.
9797
-    Calendar.prototype.clearView = function () {
9798
-        var currentView = this.view;
9799
-        this.toolbarsManager.proxyCall('deactivateButton', currentView.type);
9800
-        this.unbindViewHandlers(currentView);
9801
-        currentView.removeElement();
9802
-        currentView.unsetDate(); // so bindViewHandlers doesn't fire with old values next time
9803
-        this.view = null;
9804
-    };
9805
-    // Destroys the view, including the view object. Then, re-instantiates it and renders it.
9806
-    // Maintains the same scroll state.
9807
-    // TODO: maintain any other user-manipulated state.
9808
-    Calendar.prototype.reinitView = function () {
9809
-        var oldView = this.view;
9810
-        var scroll = oldView.queryScroll(); // wouldn't be so complicated if Calendar owned the scroll
9811
-        this.freezeContentHeight();
9812
-        this.clearView();
9813
-        this.calcSize();
9814
-        this.renderView(oldView.type); // needs the type to freshly render
9815
-        this.view.applyScroll(scroll);
9816
-        this.thawContentHeight();
9817
-    };
9818
-    // Resizing
9819
-    // -----------------------------------------------------------------------------------
9820
-    Calendar.prototype.getSuggestedViewHeight = function () {
9821
-        if (this.suggestedViewHeight == null) {
9822
-            this.calcSize();
9823
-        }
9824
-        return this.suggestedViewHeight;
9825
-    };
9826
-    Calendar.prototype.isHeightAuto = function () {
9827
-        return this.opt('contentHeight') === 'auto' || this.opt('height') === 'auto';
9828
     };
9829
-    Calendar.prototype.updateViewSize = function (isResize) {
9830
-        if (isResize === void 0) { isResize = false; }
9831
-        var view = this.view;
9832
-        var scroll;
9833
-        if (!this.ignoreUpdateViewSize && view) {
9834
-            if (isResize) {
9835
-                this.calcSize();
9836
-                scroll = view.queryScroll();
9837
+    EventPeriod.prototype.purgeAllSources = function () {
9838
+        var requestsByUid = this.requestsByUid;
9839
+        var uid;
9840
+        var request;
9841
+        var completedCnt = 0;
9842
+        for (uid in requestsByUid) {
9843
+            request = requestsByUid[uid];
9844
+            if (request.status === 'pending') {
9845
+                request.status = 'cancelled';
9846
             }
9847
-            this.ignoreUpdateViewSize++;
9848
-            view.updateSize(this.getSuggestedViewHeight(), this.isHeightAuto(), isResize);
9849
-            this.ignoreUpdateViewSize--;
9850
-            if (isResize) {
9851
-                view.applyScroll(scroll);
9852
+            else if (request.status === 'completed') {
9853
+                completedCnt++;
9854
             }
9855
-            return true; // signal success
9856
         }
9857
-    };
9858
-    Calendar.prototype.calcSize = function () {
9859
-        if (this.elementVisible()) {
9860
-            this._calcSize();
9861
+        this.requestsByUid = {};
9862
+        this.pendingCnt = 0;
9863
+        if (completedCnt) {
9864
+            this.removeAllEventDefs(); // might release
9865
         }
9866
     };
9867
-    Calendar.prototype._calcSize = function () {
9868
-        var contentHeightInput = this.opt('contentHeight');
9869
-        var heightInput = this.opt('height');
9870
-        if (typeof contentHeightInput === 'number') {
9871
-            this.suggestedViewHeight = contentHeightInput;
9872
+    // Event Definitions
9873
+    // -----------------------------------------------------------------------------------------------------------------
9874
+    EventPeriod.prototype.getEventDefByUid = function (eventDefUid) {
9875
+        return this.eventDefsByUid[eventDefUid];
9876
+    };
9877
+    EventPeriod.prototype.getEventDefsById = function (eventDefId) {
9878
+        var a = this.eventDefsById[eventDefId];
9879
+        if (a) {
9880
+            return a.slice(); // clone
9881
         }
9882
-        else if (typeof contentHeightInput === 'function') {
9883
-            this.suggestedViewHeight = contentHeightInput();
9884
+        return [];
9885
+    };
9886
+    EventPeriod.prototype.addEventDefs = function (eventDefs) {
9887
+        for (var i = 0; i < eventDefs.length; i++) {
9888
+            this.addEventDef(eventDefs[i]);
9889
         }
9890
-        else if (typeof heightInput === 'number') {
9891
-            this.suggestedViewHeight = heightInput - this.queryToolbarsHeight();
9892
+    };
9893
+    EventPeriod.prototype.addEventDef = function (eventDef) {
9894
+        var eventDefsById = this.eventDefsById;
9895
+        var eventDefId = eventDef.id;
9896
+        var eventDefs = eventDefsById[eventDefId] || (eventDefsById[eventDefId] = []);
9897
+        var eventInstances = eventDef.buildInstances(this.unzonedRange);
9898
+        var i;
9899
+        eventDefs.push(eventDef);
9900
+        this.eventDefsByUid[eventDef.uid] = eventDef;
9901
+        for (i = 0; i < eventInstances.length; i++) {
9902
+            this.addEventInstance(eventInstances[i], eventDefId);
9903
         }
9904
-        else if (typeof heightInput === 'function') {
9905
-            this.suggestedViewHeight = heightInput() - this.queryToolbarsHeight();
9906
+    };
9907
+    EventPeriod.prototype.removeEventDefsById = function (eventDefId) {
9908
+        var _this = this;
9909
+        this.getEventDefsById(eventDefId).forEach(function (eventDef) {
9910
+            _this.removeEventDef(eventDef);
9911
+        });
9912
+    };
9913
+    EventPeriod.prototype.removeAllEventDefs = function () {
9914
+        var isEmpty = $.isEmptyObject(this.eventDefsByUid);
9915
+        this.eventDefsByUid = {};
9916
+        this.eventDefsById = {};
9917
+        this.eventInstanceGroupsById = {};
9918
+        if (!isEmpty) {
9919
+            this.tryRelease();
9920
         }
9921
-        else if (heightInput === 'parent') {
9922
-            this.suggestedViewHeight = this.el.parent().height() - this.queryToolbarsHeight();
9923
+    };
9924
+    EventPeriod.prototype.removeEventDef = function (eventDef) {
9925
+        var eventDefsById = this.eventDefsById;
9926
+        var eventDefs = eventDefsById[eventDef.id];
9927
+        delete this.eventDefsByUid[eventDef.uid];
9928
+        if (eventDefs) {
9929
+            util_1.removeExact(eventDefs, eventDef);
9930
+            if (!eventDefs.length) {
9931
+                delete eventDefsById[eventDef.id];
9932
+            }
9933
+            this.removeEventInstancesForDef(eventDef);
9934
         }
9935
-        else {
9936
-            this.suggestedViewHeight = Math.round(this.contentEl.width() /
9937
-                Math.max(this.opt('aspectRatio'), .5));
9938
+    };
9939
+    // Event Instances
9940
+    // -----------------------------------------------------------------------------------------------------------------
9941
+    EventPeriod.prototype.getEventInstances = function () {
9942
+        var eventInstanceGroupsById = this.eventInstanceGroupsById;
9943
+        var eventInstances = [];
9944
+        var id;
9945
+        for (id in eventInstanceGroupsById) {
9946
+            eventInstances.push.apply(eventInstances, // append
9947
+            eventInstanceGroupsById[id].eventInstances);
9948
         }
9949
+        return eventInstances;
9950
     };
9951
-    Calendar.prototype.windowResize = function (ev) {
9952
-        if (
9953
-        // the purpose: so we don't process jqui "resize" events that have bubbled up
9954
-        // cast to any because .target, which is Element, can't be compared to window for some reason.
9955
-        ev.target === window &&
9956
-            this.view &&
9957
-            this.view.isDatesRendered) {
9958
-            if (this.updateViewSize(true)) {
9959
-                this.publiclyTrigger('windowResize', [this.view]);
9960
-            }
9961
+    EventPeriod.prototype.getEventInstancesWithId = function (eventDefId) {
9962
+        var eventInstanceGroup = this.eventInstanceGroupsById[eventDefId];
9963
+        if (eventInstanceGroup) {
9964
+            return eventInstanceGroup.eventInstances.slice(); // clone
9965
         }
9966
+        return [];
9967
     };
9968
-    /* Height "Freezing"
9969
-    -----------------------------------------------------------------------------*/
9970
-    Calendar.prototype.freezeContentHeight = function () {
9971
-        if (!(this.freezeContentHeightDepth++)) {
9972
-            this.forceFreezeContentHeight();
9973
+    EventPeriod.prototype.getEventInstancesWithoutId = function (eventDefId) {
9974
+        var eventInstanceGroupsById = this.eventInstanceGroupsById;
9975
+        var matchingInstances = [];
9976
+        var id;
9977
+        for (id in eventInstanceGroupsById) {
9978
+            if (id !== eventDefId) {
9979
+                matchingInstances.push.apply(matchingInstances, // append
9980
+                eventInstanceGroupsById[id].eventInstances);
9981
+            }
9982
         }
9983
+        return matchingInstances;
9984
     };
9985
-    Calendar.prototype.forceFreezeContentHeight = function () {
9986
-        this.contentEl.css({
9987
-            width: '100%',
9988
-            height: this.contentEl.height(),
9989
-            overflow: 'hidden'
9990
-        });
9991
+    EventPeriod.prototype.addEventInstance = function (eventInstance, eventDefId) {
9992
+        var eventInstanceGroupsById = this.eventInstanceGroupsById;
9993
+        var eventInstanceGroup = eventInstanceGroupsById[eventDefId] ||
9994
+            (eventInstanceGroupsById[eventDefId] = new EventInstanceGroup_1.default());
9995
+        eventInstanceGroup.eventInstances.push(eventInstance);
9996
+        this.tryRelease();
9997
     };
9998
-    Calendar.prototype.thawContentHeight = function () {
9999
-        this.freezeContentHeightDepth--;
10000
-        // always bring back to natural height
10001
-        this.contentEl.css({
10002
-            width: '',
10003
-            height: '',
10004
-            overflow: ''
10005
-        });
10006
-        // but if there are future thaws, re-freeze
10007
-        if (this.freezeContentHeightDepth) {
10008
-            this.forceFreezeContentHeight();
10009
+    EventPeriod.prototype.removeEventInstancesForDef = function (eventDef) {
10010
+        var eventInstanceGroupsById = this.eventInstanceGroupsById;
10011
+        var eventInstanceGroup = eventInstanceGroupsById[eventDef.id];
10012
+        var removeCnt;
10013
+        if (eventInstanceGroup) {
10014
+            removeCnt = util_1.removeMatching(eventInstanceGroup.eventInstances, function (currentEventInstance) {
10015
+                return currentEventInstance.def === eventDef;
10016
+            });
10017
+            if (!eventInstanceGroup.eventInstances.length) {
10018
+                delete eventInstanceGroupsById[eventDef.id];
10019
+            }
10020
+            if (removeCnt) {
10021
+                this.tryRelease();
10022
+            }
10023
         }
10024
     };
10025
-    // Toolbar
10026
+    // Releasing and Freezing
10027
     // -----------------------------------------------------------------------------------------------------------------
10028
-    Calendar.prototype.initToolbars = function () {
10029
-        this.header = new Toolbar_1.default(this, this.computeHeaderOptions());
10030
-        this.footer = new Toolbar_1.default(this, this.computeFooterOptions());
10031
-        this.toolbarsManager = new Iterator_1.default([this.header, this.footer]);
10032
-    };
10033
-    Calendar.prototype.computeHeaderOptions = function () {
10034
-        return {
10035
-            extraClasses: 'fc-header-toolbar',
10036
-            layout: this.opt('header')
10037
-        };
10038
+    EventPeriod.prototype.tryRelease = function () {
10039
+        if (!this.pendingCnt) {
10040
+            if (!this.freezeDepth) {
10041
+                this.release();
10042
+            }
10043
+            else {
10044
+                this.stuntedReleaseCnt++;
10045
+            }
10046
+        }
10047
     };
10048
-    Calendar.prototype.computeFooterOptions = function () {
10049
-        return {
10050
-            extraClasses: 'fc-footer-toolbar',
10051
-            layout: this.opt('footer')
10052
-        };
10053
+    EventPeriod.prototype.release = function () {
10054
+        this.releaseCnt++;
10055
+        this.trigger('release', this.eventInstanceGroupsById);
10056
     };
10057
-    // can be called repeatedly and Header will rerender
10058
-    Calendar.prototype.renderHeader = function () {
10059
-        var header = this.header;
10060
-        header.setToolbarOptions(this.computeHeaderOptions());
10061
-        header.render();
10062
-        if (header.el) {
10063
-            this.el.prepend(header.el);
10064
+    EventPeriod.prototype.whenReleased = function () {
10065
+        var _this = this;
10066
+        if (this.releaseCnt) {
10067
+            return Promise_1.default.resolve(this.eventInstanceGroupsById);
10068
         }
10069
-    };
10070
-    // can be called repeatedly and Footer will rerender
10071
-    Calendar.prototype.renderFooter = function () {
10072
-        var footer = this.footer;
10073
-        footer.setToolbarOptions(this.computeFooterOptions());
10074
-        footer.render();
10075
-        if (footer.el) {
10076
-            this.el.append(footer.el);
10077
+        else {
10078
+            return Promise_1.default.construct(function (onResolve) {
10079
+                _this.one('release', onResolve);
10080
+            });
10081
         }
10082
     };
10083
-    Calendar.prototype.setToolbarsTitle = function (title) {
10084
-        this.toolbarsManager.proxyCall('updateTitle', title);
10085
+    EventPeriod.prototype.freeze = function () {
10086
+        if (!(this.freezeDepth++)) {
10087
+            this.stuntedReleaseCnt = 0;
10088
+        }
10089
     };
10090
-    Calendar.prototype.updateToolbarButtons = function (dateProfile) {
10091
-        var now = this.getNow();
10092
-        var view = this.view;
10093
-        var todayInfo = view.dateProfileGenerator.build(now);
10094
-        var prevInfo = view.dateProfileGenerator.buildPrev(view.get('dateProfile'));
10095
-        var nextInfo = view.dateProfileGenerator.buildNext(view.get('dateProfile'));
10096
-        this.toolbarsManager.proxyCall((todayInfo.isValid && !dateProfile.currentUnzonedRange.containsDate(now)) ?
10097
-            'enableButton' :
10098
-            'disableButton', 'today');
10099
-        this.toolbarsManager.proxyCall(prevInfo.isValid ?
10100
-            'enableButton' :
10101
-            'disableButton', 'prev');
10102
-        this.toolbarsManager.proxyCall(nextInfo.isValid ?
10103
-            'enableButton' :
10104
-            'disableButton', 'next');
10105
+    EventPeriod.prototype.thaw = function () {
10106
+        if (!(--this.freezeDepth) && this.stuntedReleaseCnt && !this.pendingCnt) {
10107
+            this.release();
10108
+        }
10109
     };
10110
-    Calendar.prototype.queryToolbarsHeight = function () {
10111
-        return this.toolbarsManager.items.reduce(function (accumulator, toolbar) {
10112
-            var toolbarHeight = toolbar.el ? toolbar.el.outerHeight(true) : 0; // includes margin
10113
-            return accumulator + toolbarHeight;
10114
-        }, 0);
10115
+    return EventPeriod;
10116
+}());
10117
+exports.default = EventPeriod;
10118
+EmitterMixin_1.default.mixInto(EventPeriod);
10119
+
10120
+
10121
+/***/ }),
10122
+/* 220 */
10123
+/***/ (function(module, exports, __webpack_require__) {
10124
+
10125
+Object.defineProperty(exports, "__esModule", { value: true });
10126
+var $ = __webpack_require__(3);
10127
+var util_1 = __webpack_require__(4);
10128
+var EventPeriod_1 = __webpack_require__(219);
10129
+var ArrayEventSource_1 = __webpack_require__(56);
10130
+var EventSource_1 = __webpack_require__(6);
10131
+var EventSourceParser_1 = __webpack_require__(38);
10132
+var SingleEventDef_1 = __webpack_require__(9);
10133
+var EventInstanceGroup_1 = __webpack_require__(20);
10134
+var EmitterMixin_1 = __webpack_require__(13);
10135
+var ListenerMixin_1 = __webpack_require__(7);
10136
+var EventManager = /** @class */ (function () {
10137
+    function EventManager(calendar) {
10138
+        this.calendar = calendar;
10139
+        this.stickySource = new ArrayEventSource_1.default(calendar);
10140
+        this.otherSources = [];
10141
+    }
10142
+    EventManager.prototype.requestEvents = function (start, end, timezone, force) {
10143
+        if (force ||
10144
+            !this.currentPeriod ||
10145
+            !this.currentPeriod.isWithinRange(start, end) ||
10146
+            timezone !== this.currentPeriod.timezone) {
10147
+            this.setPeriod(// will change this.currentPeriod
10148
+            new EventPeriod_1.default(start, end, timezone));
10149
+        }
10150
+        return this.currentPeriod.whenReleased();
10151
     };
10152
-    // Selection
10153
+    // Source Adding/Removing
10154
     // -----------------------------------------------------------------------------------------------------------------
10155
-    // this public method receives start/end dates in any format, with any timezone
10156
-    Calendar.prototype.select = function (zonedStartInput, zonedEndInput) {
10157
-        this.view.select(this.buildSelectFootprint.apply(this, arguments));
10158
+    EventManager.prototype.addSource = function (eventSource) {
10159
+        this.otherSources.push(eventSource);
10160
+        if (this.currentPeriod) {
10161
+            this.currentPeriod.requestSource(eventSource); // might release
10162
+        }
10163
     };
10164
-    Calendar.prototype.unselect = function () {
10165
-        if (this.view) {
10166
-            this.view.unselect();
10167
+    EventManager.prototype.removeSource = function (doomedSource) {
10168
+        util_1.removeExact(this.otherSources, doomedSource);
10169
+        if (this.currentPeriod) {
10170
+            this.currentPeriod.purgeSource(doomedSource); // might release
10171
         }
10172
     };
10173
-    // Given arguments to the select method in the API, returns a span (unzoned start/end and other info)
10174
-    Calendar.prototype.buildSelectFootprint = function (zonedStartInput, zonedEndInput) {
10175
-        var start = this.moment(zonedStartInput).stripZone();
10176
-        var end;
10177
-        if (zonedEndInput) {
10178
-            end = this.moment(zonedEndInput).stripZone();
10179
+    EventManager.prototype.removeAllSources = function () {
10180
+        this.otherSources = [];
10181
+        if (this.currentPeriod) {
10182
+            this.currentPeriod.purgeAllSources(); // might release
10183
         }
10184
-        else if (start.hasTime()) {
10185
-            end = start.clone().add(this.defaultTimedEventDuration);
10186
+    };
10187
+    // Source Refetching
10188
+    // -----------------------------------------------------------------------------------------------------------------
10189
+    EventManager.prototype.refetchSource = function (eventSource) {
10190
+        var currentPeriod = this.currentPeriod;
10191
+        if (currentPeriod) {
10192
+            currentPeriod.freeze();
10193
+            currentPeriod.purgeSource(eventSource);
10194
+            currentPeriod.requestSource(eventSource);
10195
+            currentPeriod.thaw();
10196
         }
10197
-        else {
10198
-            end = start.clone().add(this.defaultAllDayEventDuration);
10199
+    };
10200
+    EventManager.prototype.refetchAllSources = function () {
10201
+        var currentPeriod = this.currentPeriod;
10202
+        if (currentPeriod) {
10203
+            currentPeriod.freeze();
10204
+            currentPeriod.purgeAllSources();
10205
+            currentPeriod.requestSources(this.getSources());
10206
+            currentPeriod.thaw();
10207
         }
10208
-        return new ComponentFootprint_1.default(new UnzonedRange_1.default(start, end), !start.hasTime());
10209
     };
10210
-    // Date Utils
10211
+    // Source Querying
10212
     // -----------------------------------------------------------------------------------------------------------------
10213
-    Calendar.prototype.initMomentInternals = function () {
10214
-        var _this = this;
10215
-        this.defaultAllDayEventDuration = moment.duration(this.opt('defaultAllDayEventDuration'));
10216
-        this.defaultTimedEventDuration = moment.duration(this.opt('defaultTimedEventDuration'));
10217
-        // Called immediately, and when any of the options change.
10218
-        // Happens before any internal objects rebuild or rerender, because this is very core.
10219
-        this.optionsManager.watch('buildingMomentLocale', [
10220
-            '?locale', '?monthNames', '?monthNamesShort', '?dayNames', '?dayNamesShort',
10221
-            '?firstDay', '?weekNumberCalculation'
10222
-        ], function (opts) {
10223
-            var weekNumberCalculation = opts.weekNumberCalculation;
10224
-            var firstDay = opts.firstDay;
10225
-            var _week;
10226
-            // normalize
10227
-            if (weekNumberCalculation === 'iso') {
10228
-                weekNumberCalculation = 'ISO'; // normalize
10229
-            }
10230
-            var localeData = Object.create(// make a cheap copy
10231
-            locale_1.getMomentLocaleData(opts.locale) // will fall back to en
10232
-            );
10233
-            if (opts.monthNames) {
10234
-                localeData._months = opts.monthNames;
10235
-            }
10236
-            if (opts.monthNamesShort) {
10237
-                localeData._monthsShort = opts.monthNamesShort;
10238
-            }
10239
-            if (opts.dayNames) {
10240
-                localeData._weekdays = opts.dayNames;
10241
-            }
10242
-            if (opts.dayNamesShort) {
10243
-                localeData._weekdaysShort = opts.dayNamesShort;
10244
-            }
10245
-            if (firstDay == null && weekNumberCalculation === 'ISO') {
10246
-                firstDay = 1;
10247
-            }
10248
-            if (firstDay != null) {
10249
-                _week = Object.create(localeData._week); // _week: { dow: # }
10250
-                _week.dow = firstDay;
10251
-                localeData._week = _week;
10252
-            }
10253
-            if (weekNumberCalculation === 'ISO' ||
10254
-                weekNumberCalculation === 'local' ||
10255
-                typeof weekNumberCalculation === 'function') {
10256
-                localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it
10257
-            }
10258
-            _this.localeData = localeData;
10259
-            // If the internal current date object already exists, move to new locale.
10260
-            // We do NOT need to do this technique for event dates, because this happens when converting to "segments".
10261
-            if (_this.currentDate) {
10262
-                _this.localizeMoment(_this.currentDate); // sets to localeData
10263
-            }
10264
-        });
10265
+    EventManager.prototype.getSources = function () {
10266
+        return [this.stickySource].concat(this.otherSources);
10267
     };
10268
-    // Builds a moment using the settings of the current calendar: timezone and locale.
10269
-    // Accepts anything the vanilla moment() constructor accepts.
10270
-    Calendar.prototype.moment = function () {
10271
-        var args = [];
10272
-        for (var _i = 0; _i < arguments.length; _i++) {
10273
-            args[_i] = arguments[_i];
10274
-        }
10275
-        var mom;
10276
-        if (this.opt('timezone') === 'local') {
10277
-            mom = moment_ext_1.default.apply(null, args);
10278
-            // Force the moment to be local, because momentExt doesn't guarantee it.
10279
-            if (mom.hasTime()) {
10280
-                mom.local();
10281
-            }
10282
+    // like querySources, but accepts multple match criteria (like multiple IDs)
10283
+    EventManager.prototype.multiQuerySources = function (matchInputs) {
10284
+        // coerce into an array
10285
+        if (!matchInputs) {
10286
+            matchInputs = [];
10287
         }
10288
-        else if (this.opt('timezone') === 'UTC') {
10289
-            mom = moment_ext_1.default.utc.apply(null, args); // process as UTC
10290
+        else if (!$.isArray(matchInputs)) {
10291
+            matchInputs = [matchInputs];
10292
         }
10293
-        else {
10294
-            mom = moment_ext_1.default.parseZone.apply(null, args); // let the input decide the zone
10295
+        var matchingSources = [];
10296
+        var i;
10297
+        // resolve raw inputs to real event source objects
10298
+        for (i = 0; i < matchInputs.length; i++) {
10299
+            matchingSources.push.apply(// append
10300
+            matchingSources, this.querySources(matchInputs[i]));
10301
         }
10302
-        this.localizeMoment(mom); // TODO
10303
-        return mom;
10304
+        return matchingSources;
10305
     };
10306
-    Calendar.prototype.msToMoment = function (ms, forceAllDay) {
10307
-        var mom = moment_ext_1.default.utc(ms); // TODO: optimize by using Date.UTC
10308
-        if (forceAllDay) {
10309
-            mom.stripTime();
10310
+    // matchInput can either by a real event source object, an ID, or the function/URL for the source.
10311
+    // returns an array of matching source objects.
10312
+    EventManager.prototype.querySources = function (matchInput) {
10313
+        var sources = this.otherSources;
10314
+        var i;
10315
+        var source;
10316
+        // given a proper event source object
10317
+        for (i = 0; i < sources.length; i++) {
10318
+            source = sources[i];
10319
+            if (source === matchInput) {
10320
+                return [source];
10321
+            }
10322
         }
10323
-        else {
10324
-            mom = this.applyTimezone(mom); // may or may not apply locale
10325
+        // an ID match
10326
+        source = this.getSourceById(EventSource_1.default.normalizeId(matchInput));
10327
+        if (source) {
10328
+            return [source];
10329
+        }
10330
+        // parse as an event source
10331
+        matchInput = EventSourceParser_1.default.parse(matchInput, this.calendar);
10332
+        if (matchInput) {
10333
+            return $.grep(sources, function (source) {
10334
+                return isSourcesEquivalent(matchInput, source);
10335
+            });
10336
         }
10337
-        this.localizeMoment(mom);
10338
-        return mom;
10339
     };
10340
-    Calendar.prototype.msToUtcMoment = function (ms, forceAllDay) {
10341
-        var mom = moment_ext_1.default.utc(ms); // TODO: optimize by using Date.UTC
10342
-        if (forceAllDay) {
10343
-            mom.stripTime();
10344
+    /*
10345
+    ID assumed to already be normalized
10346
+    */
10347
+    EventManager.prototype.getSourceById = function (id) {
10348
+        return $.grep(this.otherSources, function (source) {
10349
+            return source.id && source.id === id;
10350
+        })[0];
10351
+    };
10352
+    // Event-Period
10353
+    // -----------------------------------------------------------------------------------------------------------------
10354
+    EventManager.prototype.setPeriod = function (eventPeriod) {
10355
+        if (this.currentPeriod) {
10356
+            this.unbindPeriod(this.currentPeriod);
10357
+            this.currentPeriod = null;
10358
         }
10359
-        this.localizeMoment(mom);
10360
-        return mom;
10361
+        this.currentPeriod = eventPeriod;
10362
+        this.bindPeriod(eventPeriod);
10363
+        eventPeriod.requestSources(this.getSources());
10364
     };
10365
-    // Updates the given moment's locale settings to the current calendar locale settings.
10366
-    Calendar.prototype.localizeMoment = function (mom) {
10367
-        mom._locale = this.localeData;
10368
+    EventManager.prototype.bindPeriod = function (eventPeriod) {
10369
+        this.listenTo(eventPeriod, 'release', function (eventsPayload) {
10370
+            this.trigger('release', eventsPayload);
10371
+        });
10372
     };
10373
-    // Returns a boolean about whether or not the calendar knows how to calculate
10374
-    // the timezone offset of arbitrary dates in the current timezone.
10375
-    Calendar.prototype.getIsAmbigTimezone = function () {
10376
-        return this.opt('timezone') !== 'local' && this.opt('timezone') !== 'UTC';
10377
+    EventManager.prototype.unbindPeriod = function (eventPeriod) {
10378
+        this.stopListeningTo(eventPeriod);
10379
     };
10380
-    // Returns a copy of the given date in the current timezone. Has no effect on dates without times.
10381
-    Calendar.prototype.applyTimezone = function (date) {
10382
-        if (!date.hasTime()) {
10383
-            return date.clone();
10384
+    // Event Getting/Adding/Removing
10385
+    // -----------------------------------------------------------------------------------------------------------------
10386
+    EventManager.prototype.getEventDefByUid = function (uid) {
10387
+        if (this.currentPeriod) {
10388
+            return this.currentPeriod.getEventDefByUid(uid);
10389
         }
10390
-        var zonedDate = this.moment(date.toArray());
10391
-        var timeAdjust = date.time().asMilliseconds() - zonedDate.time().asMilliseconds();
10392
-        var adjustedZonedDate;
10393
-        // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396)
10394
-        if (timeAdjust) {
10395
-            adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds
10396
-            if (date.time().asMilliseconds() - adjustedZonedDate.time().asMilliseconds() === 0) {
10397
-                zonedDate = adjustedZonedDate;
10398
-            }
10399
+    };
10400
+    EventManager.prototype.addEventDef = function (eventDef, isSticky) {
10401
+        if (isSticky) {
10402
+            this.stickySource.addEventDef(eventDef);
10403
+        }
10404
+        if (this.currentPeriod) {
10405
+            this.currentPeriod.addEventDef(eventDef); // might release
10406
+        }
10407
+    };
10408
+    EventManager.prototype.removeEventDefsById = function (eventId) {
10409
+        this.getSources().forEach(function (eventSource) {
10410
+            eventSource.removeEventDefsById(eventId);
10411
+        });
10412
+        if (this.currentPeriod) {
10413
+            this.currentPeriod.removeEventDefsById(eventId); // might release
10414
+        }
10415
+    };
10416
+    EventManager.prototype.removeAllEventDefs = function () {
10417
+        this.getSources().forEach(function (eventSource) {
10418
+            eventSource.removeAllEventDefs();
10419
+        });
10420
+        if (this.currentPeriod) {
10421
+            this.currentPeriod.removeAllEventDefs();
10422
         }
10423
-        return zonedDate;
10424
     };
10425
+    // Event Mutating
10426
+    // -----------------------------------------------------------------------------------------------------------------
10427
     /*
10428
-    Assumes the footprint is non-open-ended.
10429
+    Returns an undo function.
10430
     */
10431
-    Calendar.prototype.footprintToDateProfile = function (componentFootprint, ignoreEnd) {
10432
-        if (ignoreEnd === void 0) { ignoreEnd = false; }
10433
-        var start = moment_ext_1.default.utc(componentFootprint.unzonedRange.startMs);
10434
-        var end;
10435
-        if (!ignoreEnd) {
10436
-            end = moment_ext_1.default.utc(componentFootprint.unzonedRange.endMs);
10437
-        }
10438
-        if (componentFootprint.isAllDay) {
10439
-            start.stripTime();
10440
-            if (end) {
10441
-                end.stripTime();
10442
-            }
10443
+    EventManager.prototype.mutateEventsWithId = function (eventDefId, eventDefMutation) {
10444
+        var currentPeriod = this.currentPeriod;
10445
+        var eventDefs;
10446
+        var undoFuncs = [];
10447
+        if (currentPeriod) {
10448
+            currentPeriod.freeze();
10449
+            eventDefs = currentPeriod.getEventDefsById(eventDefId);
10450
+            eventDefs.forEach(function (eventDef) {
10451
+                // add/remove esp because id might change
10452
+                currentPeriod.removeEventDef(eventDef);
10453
+                undoFuncs.push(eventDefMutation.mutateSingle(eventDef));
10454
+                currentPeriod.addEventDef(eventDef);
10455
+            });
10456
+            currentPeriod.thaw();
10457
+            return function () {
10458
+                currentPeriod.freeze();
10459
+                for (var i = 0; i < eventDefs.length; i++) {
10460
+                    currentPeriod.removeEventDef(eventDefs[i]);
10461
+                    undoFuncs[i]();
10462
+                    currentPeriod.addEventDef(eventDefs[i]);
10463
+                }
10464
+                currentPeriod.thaw();
10465
+            };
10466
         }
10467
-        else {
10468
-            start = this.applyTimezone(start);
10469
-            if (end) {
10470
-                end = this.applyTimezone(end);
10471
+        return function () { };
10472
+    };
10473
+    /*
10474
+    copies and then mutates
10475
+    */
10476
+    EventManager.prototype.buildMutatedEventInstanceGroup = function (eventDefId, eventDefMutation) {
10477
+        var eventDefs = this.getEventDefsById(eventDefId);
10478
+        var i;
10479
+        var defCopy;
10480
+        var allInstances = [];
10481
+        for (i = 0; i < eventDefs.length; i++) {
10482
+            defCopy = eventDefs[i].clone();
10483
+            if (defCopy instanceof SingleEventDef_1.default) {
10484
+                eventDefMutation.mutateSingle(defCopy);
10485
+                allInstances.push.apply(allInstances, // append
10486
+                defCopy.buildInstances());
10487
             }
10488
         }
10489
-        return new EventDateProfile_1.default(start, end, this);
10490
+        return new EventInstanceGroup_1.default(allInstances);
10491
     };
10492
-    // Returns a moment for the current date, as defined by the client's computer or from the `now` option.
10493
-    // Will return an moment with an ambiguous timezone.
10494
-    Calendar.prototype.getNow = function () {
10495
-        var now = this.opt('now');
10496
-        if (typeof now === 'function') {
10497
-            now = now();
10498
+    // Freezing
10499
+    // -----------------------------------------------------------------------------------------------------------------
10500
+    EventManager.prototype.freeze = function () {
10501
+        if (this.currentPeriod) {
10502
+            this.currentPeriod.freeze();
10503
         }
10504
-        return this.moment(now).stripZone();
10505
-    };
10506
-    // Produces a human-readable string for the given duration.
10507
-    // Side-effect: changes the locale of the given duration.
10508
-    Calendar.prototype.humanizeDuration = function (duration) {
10509
-        return duration.locale(this.opt('locale')).humanize();
10510
     };
10511
-    // will return `null` if invalid range
10512
-    Calendar.prototype.parseUnzonedRange = function (rangeInput) {
10513
-        var start = null;
10514
-        var end = null;
10515
-        if (rangeInput.start) {
10516
-            start = this.moment(rangeInput.start).stripZone();
10517
-        }
10518
-        if (rangeInput.end) {
10519
-            end = this.moment(rangeInput.end).stripZone();
10520
-        }
10521
-        if (!start && !end) {
10522
-            return null;
10523
-        }
10524
-        if (start && end && end.isBefore(start)) {
10525
-            return null;
10526
+    EventManager.prototype.thaw = function () {
10527
+        if (this.currentPeriod) {
10528
+            this.currentPeriod.thaw();
10529
         }
10530
-        return new UnzonedRange_1.default(start, end);
10531
     };
10532
-    // Event-Date Utilities
10533
-    // -----------------------------------------------------------------------------------------------------------------
10534
-    Calendar.prototype.initEventManager = function () {
10535
-        var _this = this;
10536
-        var eventManager = new EventManager_1.default(this);
10537
-        var rawSources = this.opt('eventSources') || [];
10538
-        var singleRawSource = this.opt('events');
10539
-        this.eventManager = eventManager;
10540
-        if (singleRawSource) {
10541
-            rawSources.unshift(singleRawSource);
10542
-        }
10543
-        eventManager.on('release', function (eventsPayload) {
10544
-            _this.trigger('eventsReset', eventsPayload);
10545
-        });
10546
-        eventManager.freeze();
10547
-        rawSources.forEach(function (rawSource) {
10548
-            var source = EventSourceParser_1.default.parse(rawSource, _this);
10549
-            if (source) {
10550
-                eventManager.addSource(source);
10551
-            }
10552
-        });
10553
-        eventManager.thaw();
10554
+    // methods that simply forward to EventPeriod
10555
+    EventManager.prototype.getEventDefsById = function (eventDefId) {
10556
+        return this.currentPeriod.getEventDefsById(eventDefId);
10557
     };
10558
-    Calendar.prototype.requestEvents = function (start, end) {
10559
-        return this.eventManager.requestEvents(start, end, this.opt('timezone'), !this.opt('lazyFetching'));
10560
+    EventManager.prototype.getEventInstances = function () {
10561
+        return this.currentPeriod.getEventInstances();
10562
     };
10563
-    // Get an event's normalized end date. If not present, calculate it from the defaults.
10564
-    Calendar.prototype.getEventEnd = function (event) {
10565
-        if (event.end) {
10566
-            return event.end.clone();
10567
-        }
10568
-        else {
10569
-            return this.getDefaultEventEnd(event.allDay, event.start);
10570
-        }
10571
+    EventManager.prototype.getEventInstancesWithId = function (eventDefId) {
10572
+        return this.currentPeriod.getEventInstancesWithId(eventDefId);
10573
     };
10574
-    // Given an event's allDay status and start date, return what its fallback end date should be.
10575
-    // TODO: rename to computeDefaultEventEnd
10576
-    Calendar.prototype.getDefaultEventEnd = function (allDay, zonedStart) {
10577
-        var end = zonedStart.clone();
10578
-        if (allDay) {
10579
-            end.stripTime().add(this.defaultAllDayEventDuration);
10580
+    EventManager.prototype.getEventInstancesWithoutId = function (eventDefId) {
10581
+        return this.currentPeriod.getEventInstancesWithoutId(eventDefId);
10582
+    };
10583
+    return EventManager;
10584
+}());
10585
+exports.default = EventManager;
10586
+EmitterMixin_1.default.mixInto(EventManager);
10587
+ListenerMixin_1.default.mixInto(EventManager);
10588
+function isSourcesEquivalent(source0, source1) {
10589
+    return source0.getPrimitive() === source1.getPrimitive();
10590
+}
10591
+
10592
+
10593
+/***/ }),
10594
+/* 221 */
10595
+/***/ (function(module, exports, __webpack_require__) {
10596
+
10597
+Object.defineProperty(exports, "__esModule", { value: true });
10598
+var tslib_1 = __webpack_require__(2);
10599
+var Theme_1 = __webpack_require__(22);
10600
+var StandardTheme = /** @class */ (function (_super) {
10601
+    tslib_1.__extends(StandardTheme, _super);
10602
+    function StandardTheme() {
10603
+        return _super !== null && _super.apply(this, arguments) || this;
10604
+    }
10605
+    return StandardTheme;
10606
+}(Theme_1.default));
10607
+exports.default = StandardTheme;
10608
+StandardTheme.prototype.classes = {
10609
+    widget: 'fc-unthemed',
10610
+    widgetHeader: 'fc-widget-header',
10611
+    widgetContent: 'fc-widget-content',
10612
+    buttonGroup: 'fc-button-group',
10613
+    button: 'fc-button',
10614
+    cornerLeft: 'fc-corner-left',
10615
+    cornerRight: 'fc-corner-right',
10616
+    stateDefault: 'fc-state-default',
10617
+    stateActive: 'fc-state-active',
10618
+    stateDisabled: 'fc-state-disabled',
10619
+    stateHover: 'fc-state-hover',
10620
+    stateDown: 'fc-state-down',
10621
+    popoverHeader: 'fc-widget-header',
10622
+    popoverContent: 'fc-widget-content',
10623
+    // day grid
10624
+    headerRow: 'fc-widget-header',
10625
+    dayRow: 'fc-widget-content',
10626
+    // list view
10627
+    listView: 'fc-widget-content'
10628
+};
10629
+StandardTheme.prototype.baseIconClass = 'fc-icon';
10630
+StandardTheme.prototype.iconClasses = {
10631
+    close: 'fc-icon-x',
10632
+    prev: 'fc-icon-left-single-arrow',
10633
+    next: 'fc-icon-right-single-arrow',
10634
+    prevYear: 'fc-icon-left-double-arrow',
10635
+    nextYear: 'fc-icon-right-double-arrow'
10636
+};
10637
+StandardTheme.prototype.iconOverrideOption = 'buttonIcons';
10638
+StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon';
10639
+StandardTheme.prototype.iconOverridePrefix = 'fc-icon-';
10640
+
10641
+
10642
+/***/ }),
10643
+/* 222 */
10644
+/***/ (function(module, exports, __webpack_require__) {
10645
+
10646
+Object.defineProperty(exports, "__esModule", { value: true });
10647
+var tslib_1 = __webpack_require__(2);
10648
+var Theme_1 = __webpack_require__(22);
10649
+var JqueryUiTheme = /** @class */ (function (_super) {
10650
+    tslib_1.__extends(JqueryUiTheme, _super);
10651
+    function JqueryUiTheme() {
10652
+        return _super !== null && _super.apply(this, arguments) || this;
10653
+    }
10654
+    return JqueryUiTheme;
10655
+}(Theme_1.default));
10656
+exports.default = JqueryUiTheme;
10657
+JqueryUiTheme.prototype.classes = {
10658
+    widget: 'ui-widget',
10659
+    widgetHeader: 'ui-widget-header',
10660
+    widgetContent: 'ui-widget-content',
10661
+    buttonGroup: 'fc-button-group',
10662
+    button: 'ui-button',
10663
+    cornerLeft: 'ui-corner-left',
10664
+    cornerRight: 'ui-corner-right',
10665
+    stateDefault: 'ui-state-default',
10666
+    stateActive: 'ui-state-active',
10667
+    stateDisabled: 'ui-state-disabled',
10668
+    stateHover: 'ui-state-hover',
10669
+    stateDown: 'ui-state-down',
10670
+    today: 'ui-state-highlight',
10671
+    popoverHeader: 'ui-widget-header',
10672
+    popoverContent: 'ui-widget-content',
10673
+    // day grid
10674
+    headerRow: 'ui-widget-header',
10675
+    dayRow: 'ui-widget-content',
10676
+    // list view
10677
+    listView: 'ui-widget-content'
10678
+};
10679
+JqueryUiTheme.prototype.baseIconClass = 'ui-icon';
10680
+JqueryUiTheme.prototype.iconClasses = {
10681
+    close: 'ui-icon-closethick',
10682
+    prev: 'ui-icon-circle-triangle-w',
10683
+    next: 'ui-icon-circle-triangle-e',
10684
+    prevYear: 'ui-icon-seek-prev',
10685
+    nextYear: 'ui-icon-seek-next'
10686
+};
10687
+JqueryUiTheme.prototype.iconOverrideOption = 'themeButtonIcons';
10688
+JqueryUiTheme.prototype.iconOverrideCustomButtonOption = 'themeIcon';
10689
+JqueryUiTheme.prototype.iconOverridePrefix = 'ui-icon-';
10690
+
10691
+
10692
+/***/ }),
10693
+/* 223 */
10694
+/***/ (function(module, exports, __webpack_require__) {
10695
+
10696
+Object.defineProperty(exports, "__esModule", { value: true });
10697
+var tslib_1 = __webpack_require__(2);
10698
+var $ = __webpack_require__(3);
10699
+var Promise_1 = __webpack_require__(21);
10700
+var EventSource_1 = __webpack_require__(6);
10701
+var FuncEventSource = /** @class */ (function (_super) {
10702
+    tslib_1.__extends(FuncEventSource, _super);
10703
+    function FuncEventSource() {
10704
+        return _super !== null && _super.apply(this, arguments) || this;
10705
+    }
10706
+    FuncEventSource.parse = function (rawInput, calendar) {
10707
+        var rawProps;
10708
+        // normalize raw input
10709
+        if ($.isFunction(rawInput.events)) { // extended form
10710
+            rawProps = rawInput;
10711
         }
10712
-        else {
10713
-            end.add(this.defaultTimedEventDuration);
10714
+        else if ($.isFunction(rawInput)) { // short form
10715
+            rawProps = { events: rawInput };
10716
         }
10717
-        if (this.getIsAmbigTimezone()) {
10718
-            end.stripZone(); // we don't know what the tzo should be
10719
+        if (rawProps) {
10720
+            return EventSource_1.default.parse.call(this, rawProps, calendar);
10721
         }
10722
-        return end;
10723
+        return false;
10724
     };
10725
-    // Public Events API
10726
-    // -----------------------------------------------------------------------------------------------------------------
10727
-    Calendar.prototype.rerenderEvents = function () {
10728
-        this.view.flash('displayingEvents');
10729
+    FuncEventSource.prototype.fetch = function (start, end, timezone) {
10730
+        var _this = this;
10731
+        this.calendar.pushLoading();
10732
+        return Promise_1.default.construct(function (onResolve) {
10733
+            _this.func.call(_this.calendar, start.clone(), end.clone(), timezone, function (rawEventDefs) {
10734
+                _this.calendar.popLoading();
10735
+                onResolve(_this.parseEventDefs(rawEventDefs));
10736
+            });
10737
+        });
10738
     };
10739
-    Calendar.prototype.refetchEvents = function () {
10740
-        this.eventManager.refetchAllSources();
10741
+    FuncEventSource.prototype.getPrimitive = function () {
10742
+        return this.func;
10743
     };
10744
-    Calendar.prototype.renderEvents = function (eventInputs, isSticky) {
10745
-        this.eventManager.freeze();
10746
-        for (var i = 0; i < eventInputs.length; i++) {
10747
-            this.renderEvent(eventInputs[i], isSticky);
10748
-        }
10749
-        this.eventManager.thaw();
10750
+    FuncEventSource.prototype.applyManualStandardProps = function (rawProps) {
10751
+        var superSuccess = _super.prototype.applyManualStandardProps.call(this, rawProps);
10752
+        this.func = rawProps.events;
10753
+        return superSuccess;
10754
     };
10755
-    Calendar.prototype.renderEvent = function (eventInput, isSticky) {
10756
-        if (isSticky === void 0) { isSticky = false; }
10757
-        var eventManager = this.eventManager;
10758
-        var eventDef = EventDefParser_1.default.parse(eventInput, eventInput.source || eventManager.stickySource);
10759
-        if (eventDef) {
10760
-            eventManager.addEventDef(eventDef, isSticky);
10761
+    return FuncEventSource;
10762
+}(EventSource_1.default));
10763
+exports.default = FuncEventSource;
10764
+FuncEventSource.defineStandardProps({
10765
+    events: false // don't automatically transfer
10766
+});
10767
+
10768
+
10769
+/***/ }),
10770
+/* 224 */
10771
+/***/ (function(module, exports, __webpack_require__) {
10772
+
10773
+Object.defineProperty(exports, "__esModule", { value: true });
10774
+var tslib_1 = __webpack_require__(2);
10775
+var $ = __webpack_require__(3);
10776
+var util_1 = __webpack_require__(4);
10777
+var Promise_1 = __webpack_require__(21);
10778
+var EventSource_1 = __webpack_require__(6);
10779
+var JsonFeedEventSource = /** @class */ (function (_super) {
10780
+    tslib_1.__extends(JsonFeedEventSource, _super);
10781
+    function JsonFeedEventSource() {
10782
+        return _super !== null && _super.apply(this, arguments) || this;
10783
+    }
10784
+    JsonFeedEventSource.parse = function (rawInput, calendar) {
10785
+        var rawProps;
10786
+        // normalize raw input
10787
+        if (typeof rawInput.url === 'string') { // extended form
10788
+            rawProps = rawInput;
10789
         }
10790
-    };
10791
-    // legacyQuery operates on legacy event instance objects
10792
-    Calendar.prototype.removeEvents = function (legacyQuery) {
10793
-        var eventManager = this.eventManager;
10794
-        var legacyInstances = [];
10795
-        var idMap = {};
10796
-        var eventDef;
10797
-        var i;
10798
-        if (legacyQuery == null) {
10799
-            eventManager.removeAllEventDefs(); // persist=true
10800
+        else if (typeof rawInput === 'string') { // short form
10801
+            rawProps = { url: rawInput };
10802
         }
10803
-        else {
10804
-            eventManager.getEventInstances().forEach(function (eventInstance) {
10805
-                legacyInstances.push(eventInstance.toLegacy());
10806
-            });
10807
-            legacyInstances = filterLegacyEventInstances(legacyInstances, legacyQuery);
10808
-            // compute unique IDs
10809
-            for (i = 0; i < legacyInstances.length; i++) {
10810
-                eventDef = this.eventManager.getEventDefByUid(legacyInstances[i]._id);
10811
-                idMap[eventDef.id] = true;
10812
-            }
10813
-            eventManager.freeze();
10814
-            for (i in idMap) {
10815
-                eventManager.removeEventDefsById(i); // persist=true
10816
-            }
10817
-            eventManager.thaw();
10818
+        if (rawProps) {
10819
+            return EventSource_1.default.parse.call(this, rawProps, calendar);
10820
         }
10821
+        return false;
10822
     };
10823
-    // legacyQuery operates on legacy event instance objects
10824
-    Calendar.prototype.clientEvents = function (legacyQuery) {
10825
-        var legacyEventInstances = [];
10826
-        this.eventManager.getEventInstances().forEach(function (eventInstance) {
10827
-            legacyEventInstances.push(eventInstance.toLegacy());
10828
+    JsonFeedEventSource.prototype.fetch = function (start, end, timezone) {
10829
+        var _this = this;
10830
+        var ajaxSettings = this.ajaxSettings;
10831
+        var onSuccess = ajaxSettings.success;
10832
+        var onError = ajaxSettings.error;
10833
+        var requestParams = this.buildRequestParams(start, end, timezone);
10834
+        // todo: eventually handle the promise's then,
10835
+        // don't intercept success/error
10836
+        // tho will be a breaking API change
10837
+        this.calendar.pushLoading();
10838
+        return Promise_1.default.construct(function (onResolve, onReject) {
10839
+            $.ajax($.extend({}, // destination
10840
+            JsonFeedEventSource.AJAX_DEFAULTS, ajaxSettings, {
10841
+                url: _this.url,
10842
+                data: requestParams,
10843
+                success: function (rawEventDefs, status, xhr) {
10844
+                    var callbackRes;
10845
+                    _this.calendar.popLoading();
10846
+                    if (rawEventDefs) {
10847
+                        callbackRes = util_1.applyAll(onSuccess, _this, [rawEventDefs, status, xhr]); // redirect `this`
10848
+                        if ($.isArray(callbackRes)) {
10849
+                            rawEventDefs = callbackRes;
10850
+                        }
10851
+                        onResolve(_this.parseEventDefs(rawEventDefs));
10852
+                    }
10853
+                    else {
10854
+                        onReject();
10855
+                    }
10856
+                },
10857
+                error: function (xhr, statusText, errorThrown) {
10858
+                    _this.calendar.popLoading();
10859
+                    util_1.applyAll(onError, _this, [xhr, statusText, errorThrown]); // redirect `this`
10860
+                    onReject();
10861
+                }
10862
+            }));
10863
         });
10864
-        return filterLegacyEventInstances(legacyEventInstances, legacyQuery);
10865
     };
10866
-    Calendar.prototype.updateEvents = function (eventPropsArray) {
10867
-        this.eventManager.freeze();
10868
-        for (var i = 0; i < eventPropsArray.length; i++) {
10869
-            this.updateEvent(eventPropsArray[i]);
10870
+    JsonFeedEventSource.prototype.buildRequestParams = function (start, end, timezone) {
10871
+        var calendar = this.calendar;
10872
+        var ajaxSettings = this.ajaxSettings;
10873
+        var startParam;
10874
+        var endParam;
10875
+        var timezoneParam;
10876
+        var customRequestParams;
10877
+        var params = {};
10878
+        startParam = this.startParam;
10879
+        if (startParam == null) {
10880
+            startParam = calendar.opt('startParam');
10881
         }
10882
-        this.eventManager.thaw();
10883
-    };
10884
-    Calendar.prototype.updateEvent = function (eventProps) {
10885
-        var eventDef = this.eventManager.getEventDefByUid(eventProps._id);
10886
-        var eventInstance;
10887
-        var eventDefMutation;
10888
-        if (eventDef instanceof SingleEventDef_1.default) {
10889
-            eventInstance = eventDef.buildInstance();
10890
-            eventDefMutation = EventDefMutation_1.default.createFromRawProps(eventInstance, eventProps, // raw props
10891
-            null // largeUnit -- who uses it?
10892
-            );
10893
-            this.eventManager.mutateEventsWithId(eventDef.id, eventDefMutation); // will release
10894
+        endParam = this.endParam;
10895
+        if (endParam == null) {
10896
+            endParam = calendar.opt('endParam');
10897
         }
10898
-    };
10899
-    // Public Event Sources API
10900
-    // ------------------------------------------------------------------------------------
10901
-    Calendar.prototype.getEventSources = function () {
10902
-        return this.eventManager.otherSources.slice(); // clone
10903
-    };
10904
-    Calendar.prototype.getEventSourceById = function (id) {
10905
-        return this.eventManager.getSourceById(EventSource_1.default.normalizeId(id));
10906
-    };
10907
-    Calendar.prototype.addEventSource = function (sourceInput) {
10908
-        var source = EventSourceParser_1.default.parse(sourceInput, this);
10909
-        if (source) {
10910
-            this.eventManager.addSource(source);
10911
+        timezoneParam = this.timezoneParam;
10912
+        if (timezoneParam == null) {
10913
+            timezoneParam = calendar.opt('timezoneParam');
10914
         }
10915
-    };
10916
-    Calendar.prototype.removeEventSources = function (sourceMultiQuery) {
10917
-        var eventManager = this.eventManager;
10918
-        var sources;
10919
-        var i;
10920
-        if (sourceMultiQuery == null) {
10921
-            this.eventManager.removeAllSources();
10922
+        // retrieve any outbound GET/POST $.ajax data from the options
10923
+        if ($.isFunction(ajaxSettings.data)) {
10924
+            // supplied as a function that returns a key/value object
10925
+            customRequestParams = ajaxSettings.data();
10926
         }
10927
         else {
10928
-            sources = eventManager.multiQuerySources(sourceMultiQuery);
10929
-            eventManager.freeze();
10930
-            for (i = 0; i < sources.length; i++) {
10931
-                eventManager.removeSource(sources[i]);
10932
-            }
10933
-            eventManager.thaw();
10934
+            // probably supplied as a straight key/value object
10935
+            customRequestParams = ajaxSettings.data || {};
10936
         }
10937
-    };
10938
-    Calendar.prototype.removeEventSource = function (sourceQuery) {
10939
-        var eventManager = this.eventManager;
10940
-        var sources = eventManager.querySources(sourceQuery);
10941
-        var i;
10942
-        eventManager.freeze();
10943
-        for (i = 0; i < sources.length; i++) {
10944
-            eventManager.removeSource(sources[i]);
10945
+        $.extend(params, customRequestParams);
10946
+        params[startParam] = start.format();
10947
+        params[endParam] = end.format();
10948
+        if (timezone && timezone !== 'local') {
10949
+            params[timezoneParam] = timezone;
10950
         }
10951
-        eventManager.thaw();
10952
+        return params;
10953
     };
10954
-    Calendar.prototype.refetchEventSources = function (sourceMultiQuery) {
10955
-        var eventManager = this.eventManager;
10956
-        var sources = eventManager.multiQuerySources(sourceMultiQuery);
10957
-        var i;
10958
-        eventManager.freeze();
10959
-        for (i = 0; i < sources.length; i++) {
10960
-            eventManager.refetchSource(sources[i]);
10961
+    JsonFeedEventSource.prototype.getPrimitive = function () {
10962
+        return this.url;
10963
+    };
10964
+    JsonFeedEventSource.prototype.applyMiscProps = function (rawProps) {
10965
+        this.ajaxSettings = rawProps;
10966
+    };
10967
+    JsonFeedEventSource.AJAX_DEFAULTS = {
10968
+        dataType: 'json',
10969
+        cache: false
10970
+    };
10971
+    return JsonFeedEventSource;
10972
+}(EventSource_1.default));
10973
+exports.default = JsonFeedEventSource;
10974
+JsonFeedEventSource.defineStandardProps({
10975
+    // automatically transfer (true)...
10976
+    url: true,
10977
+    startParam: true,
10978
+    endParam: true,
10979
+    timezoneParam: true
10980
+});
10981
+
10982
+
10983
+/***/ }),
10984
+/* 225 */
10985
+/***/ (function(module, exports) {
10986
+
10987
+Object.defineProperty(exports, "__esModule", { value: true });
10988
+var Iterator = /** @class */ (function () {
10989
+    function Iterator(items) {
10990
+        this.items = items || [];
10991
+    }
10992
+    /* Calls a method on every item passing the arguments through */
10993
+    Iterator.prototype.proxyCall = function (methodName) {
10994
+        var args = [];
10995
+        for (var _i = 1; _i < arguments.length; _i++) {
10996
+            args[_i - 1] = arguments[_i];
10997
         }
10998
-        eventManager.thaw();
10999
+        var results = [];
11000
+        this.items.forEach(function (item) {
11001
+            results.push(item[methodName].apply(item, args));
11002
+        });
11003
+        return results;
11004
     };
11005
-    // not for internal use. use options module directly instead.
11006
-    Calendar.defaults = options_1.globalDefaults;
11007
-    Calendar.englishDefaults = options_1.englishDefaults;
11008
-    Calendar.rtlDefaults = options_1.rtlDefaults;
11009
-    return Calendar;
11010
+    return Iterator;
11011
 }());
11012
-exports.default = Calendar;
11013
-EmitterMixin_1.default.mixInto(Calendar);
11014
-ListenerMixin_1.default.mixInto(Calendar);
11015
-function filterLegacyEventInstances(legacyEventInstances, legacyQuery) {
11016
-    if (legacyQuery == null) {
11017
-        return legacyEventInstances;
11018
-    }
11019
-    else if ($.isFunction(legacyQuery)) {
11020
-        return legacyEventInstances.filter(legacyQuery);
11021
-    }
11022
-    else {
11023
-        legacyQuery += ''; // normalize to string
11024
-        return legacyEventInstances.filter(function (legacyEventInstance) {
11025
-            // soft comparison because id not be normalized to string
11026
-            // tslint:disable-next-line
11027
-            return legacyEventInstance.id == legacyQuery ||
11028
-                legacyEventInstance._id === legacyQuery; // can specify internal id, but must exactly match
11029
-        });
11030
-    }
11031
-}
11032
+exports.default = Iterator;
11033
 
11034
 
11035
 /***/ }),
11036
-/* 221 */
11037
+/* 226 */
11038
 /***/ (function(module, exports, __webpack_require__) {
11039
 
11040
 Object.defineProperty(exports, "__esModule", { value: true });
11041
-var moment = __webpack_require__(0);
11042
+var $ = __webpack_require__(3);
11043
 var util_1 = __webpack_require__(4);
11044
-var UnzonedRange_1 = __webpack_require__(5);
11045
-var DateProfileGenerator = /** @class */ (function () {
11046
-    function DateProfileGenerator(_view) {
11047
-        this._view = _view;
11048
+var ListenerMixin_1 = __webpack_require__(7);
11049
+/* Creates a clone of an element and lets it track the mouse as it moves
11050
+----------------------------------------------------------------------------------------------------------------------*/
11051
+var MouseFollower = /** @class */ (function () {
11052
+    function MouseFollower(sourceEl, options) {
11053
+        this.isFollowing = false;
11054
+        this.isHidden = false;
11055
+        this.isAnimating = false; // doing the revert animation?
11056
+        this.options = options = options || {};
11057
+        this.sourceEl = sourceEl;
11058
+        this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
11059
     }
11060
-    DateProfileGenerator.prototype.opt = function (name) {
11061
-        return this._view.opt(name);
11062
-    };
11063
-    DateProfileGenerator.prototype.trimHiddenDays = function (unzonedRange) {
11064
-        return this._view.trimHiddenDays(unzonedRange);
11065
-    };
11066
-    DateProfileGenerator.prototype.msToUtcMoment = function (ms, forceAllDay) {
11067
-        return this._view.calendar.msToUtcMoment(ms, forceAllDay);
11068
-    };
11069
-    /* Date Range Computation
11070
-    ------------------------------------------------------------------------------------------------------------------*/
11071
-    // Builds a structure with info about what the dates/ranges will be for the "prev" view.
11072
-    DateProfileGenerator.prototype.buildPrev = function (currentDateProfile) {
11073
-        var prevDate = currentDateProfile.date.clone()
11074
-            .startOf(currentDateProfile.currentRangeUnit)
11075
-            .subtract(currentDateProfile.dateIncrement);
11076
-        return this.build(prevDate, -1);
11077
-    };
11078
-    // Builds a structure with info about what the dates/ranges will be for the "next" view.
11079
-    DateProfileGenerator.prototype.buildNext = function (currentDateProfile) {
11080
-        var nextDate = currentDateProfile.date.clone()
11081
-            .startOf(currentDateProfile.currentRangeUnit)
11082
-            .add(currentDateProfile.dateIncrement);
11083
-        return this.build(nextDate, 1);
11084
-    };
11085
-    // Builds a structure holding dates/ranges for rendering around the given date.
11086
-    // Optional direction param indicates whether the date is being incremented/decremented
11087
-    // from its previous value. decremented = -1, incremented = 1 (default).
11088
-    DateProfileGenerator.prototype.build = function (date, direction, forceToValid) {
11089
-        if (forceToValid === void 0) { forceToValid = false; }
11090
-        var isDateAllDay = !date.hasTime();
11091
-        var validUnzonedRange;
11092
-        var minTime = null;
11093
-        var maxTime = null;
11094
-        var currentInfo;
11095
-        var isRangeAllDay;
11096
-        var renderUnzonedRange;
11097
-        var activeUnzonedRange;
11098
-        var isValid;
11099
-        validUnzonedRange = this.buildValidRange();
11100
-        validUnzonedRange = this.trimHiddenDays(validUnzonedRange);
11101
-        if (forceToValid) {
11102
-            date = this.msToUtcMoment(validUnzonedRange.constrainDate(date), // returns MS
11103
-            isDateAllDay);
11104
-        }
11105
-        currentInfo = this.buildCurrentRangeInfo(date, direction);
11106
-        isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit);
11107
-        renderUnzonedRange = this.buildRenderRange(this.trimHiddenDays(currentInfo.unzonedRange), currentInfo.unit, isRangeAllDay);
11108
-        renderUnzonedRange = this.trimHiddenDays(renderUnzonedRange);
11109
-        activeUnzonedRange = renderUnzonedRange.clone();
11110
-        if (!this.opt('showNonCurrentDates')) {
11111
-            activeUnzonedRange = activeUnzonedRange.intersect(currentInfo.unzonedRange);
11112
-        }
11113
-        minTime = moment.duration(this.opt('minTime'));
11114
-        maxTime = moment.duration(this.opt('maxTime'));
11115
-        activeUnzonedRange = this.adjustActiveRange(activeUnzonedRange, minTime, maxTime);
11116
-        activeUnzonedRange = activeUnzonedRange.intersect(validUnzonedRange); // might return null
11117
-        if (activeUnzonedRange) {
11118
-            date = this.msToUtcMoment(activeUnzonedRange.constrainDate(date), // returns MS
11119
-            isDateAllDay);
11120
+    // Causes the element to start following the mouse
11121
+    MouseFollower.prototype.start = function (ev) {
11122
+        if (!this.isFollowing) {
11123
+            this.isFollowing = true;
11124
+            this.y0 = util_1.getEvY(ev);
11125
+            this.x0 = util_1.getEvX(ev);
11126
+            this.topDelta = 0;
11127
+            this.leftDelta = 0;
11128
+            if (!this.isHidden) {
11129
+                this.updatePosition();
11130
+            }
11131
+            if (util_1.getEvIsTouch(ev)) {
11132
+                this.listenTo($(document), 'touchmove', this.handleMove);
11133
+            }
11134
+            else {
11135
+                this.listenTo($(document), 'mousemove', this.handleMove);
11136
+            }
11137
         }
11138
-        // it's invalid if the originally requested date is not contained,
11139
-        // or if the range is completely outside of the valid range.
11140
-        isValid = currentInfo.unzonedRange.intersectsWith(validUnzonedRange);
11141
-        return {
11142
-            // constraint for where prev/next operations can go and where events can be dragged/resized to.
11143
-            // an object with optional start and end properties.
11144
-            validUnzonedRange: validUnzonedRange,
11145
-            // range the view is formally responsible for.
11146
-            // for example, a month view might have 1st-31st, excluding padded dates
11147
-            currentUnzonedRange: currentInfo.unzonedRange,
11148
-            // name of largest unit being displayed, like "month" or "week"
11149
-            currentRangeUnit: currentInfo.unit,
11150
-            isRangeAllDay: isRangeAllDay,
11151
-            // dates that display events and accept drag-n-drop
11152
-            // will be `null` if no dates accept events
11153
-            activeUnzonedRange: activeUnzonedRange,
11154
-            // date range with a rendered skeleton
11155
-            // includes not-active days that need some sort of DOM
11156
-            renderUnzonedRange: renderUnzonedRange,
11157
-            // Duration object that denotes the first visible time of any given day
11158
-            minTime: minTime,
11159
-            // Duration object that denotes the exclusive visible end time of any given day
11160
-            maxTime: maxTime,
11161
-            isValid: isValid,
11162
-            date: date,
11163
-            // how far the current date will move for a prev/next operation
11164
-            dateIncrement: this.buildDateIncrement(currentInfo.duration)
11165
-            // pass a fallback (might be null) ^
11166
-        };
11167
-    };
11168
-    // Builds an object with optional start/end properties.
11169
-    // Indicates the minimum/maximum dates to display.
11170
-    // not responsible for trimming hidden days.
11171
-    DateProfileGenerator.prototype.buildValidRange = function () {
11172
-        return this._view.getUnzonedRangeOption('validRange', this._view.calendar.getNow()) ||
11173
-            new UnzonedRange_1.default(); // completely open-ended
11174
     };
11175
-    // Builds a structure with info about the "current" range, the range that is
11176
-    // highlighted as being the current month for example.
11177
-    // See build() for a description of `direction`.
11178
-    // Guaranteed to have `range` and `unit` properties. `duration` is optional.
11179
-    // TODO: accept a MS-time instead of a moment `date`?
11180
-    DateProfileGenerator.prototype.buildCurrentRangeInfo = function (date, direction) {
11181
-        var viewSpec = this._view.viewSpec;
11182
-        var duration = null;
11183
-        var unit = null;
11184
-        var unzonedRange = null;
11185
-        var dayCount;
11186
-        if (viewSpec.duration) {
11187
-            duration = viewSpec.duration;
11188
-            unit = viewSpec.durationUnit;
11189
-            unzonedRange = this.buildRangeFromDuration(date, direction, duration, unit);
11190
+    // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
11191
+    // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
11192
+    MouseFollower.prototype.stop = function (shouldRevert, callback) {
11193
+        var _this = this;
11194
+        var revertDuration = this.options.revertDuration;
11195
+        var complete = function () {
11196
+            _this.isAnimating = false;
11197
+            _this.removeElement();
11198
+            _this.top0 = _this.left0 = null; // reset state for future updatePosition calls
11199
+            if (callback) {
11200
+                callback();
11201
+            }
11202
+        };
11203
+        if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
11204
+            this.isFollowing = false;
11205
+            this.stopListeningTo($(document));
11206
+            if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
11207
+                this.isAnimating = true;
11208
+                this.el.animate({
11209
+                    top: this.top0,
11210
+                    left: this.left0
11211
+                }, {
11212
+                    duration: revertDuration,
11213
+                    complete: complete
11214
+                });
11215
+            }
11216
+            else {
11217
+                complete();
11218
+            }
11219
         }
11220
-        else if ((dayCount = this.opt('dayCount'))) {
11221
-            unit = 'day';
11222
-            unzonedRange = this.buildRangeFromDayCount(date, direction, dayCount);
11223
+    };
11224
+    // Gets the tracking element. Create it if necessary
11225
+    MouseFollower.prototype.getEl = function () {
11226
+        var el = this.el;
11227
+        if (!el) {
11228
+            el = this.el = this.sourceEl.clone()
11229
+                .addClass(this.options.additionalClass || '')
11230
+                .css({
11231
+                position: 'absolute',
11232
+                visibility: '',
11233
+                display: this.isHidden ? 'none' : '',
11234
+                margin: 0,
11235
+                right: 'auto',
11236
+                bottom: 'auto',
11237
+                width: this.sourceEl.width(),
11238
+                height: this.sourceEl.height(),
11239
+                opacity: this.options.opacity || '',
11240
+                zIndex: this.options.zIndex
11241
+            });
11242
+            // we don't want long taps or any mouse interaction causing selection/menus.
11243
+            // would use preventSelection(), but that prevents selectstart, causing problems.
11244
+            el.addClass('fc-unselectable');
11245
+            el.appendTo(this.parentEl);
11246
         }
11247
-        else if ((unzonedRange = this.buildCustomVisibleRange(date))) {
11248
-            unit = util_1.computeGreatestUnit(unzonedRange.getStart(), unzonedRange.getEnd());
11249
+        return el;
11250
+    };
11251
+    // Removes the tracking element if it has already been created
11252
+    MouseFollower.prototype.removeElement = function () {
11253
+        if (this.el) {
11254
+            this.el.remove();
11255
+            this.el = null;
11256
         }
11257
-        else {
11258
-            duration = this.getFallbackDuration();
11259
-            unit = util_1.computeGreatestUnit(duration);
11260
-            unzonedRange = this.buildRangeFromDuration(date, direction, duration, unit);
11261
+    };
11262
+    // Update the CSS position of the tracking element
11263
+    MouseFollower.prototype.updatePosition = function () {
11264
+        var sourceOffset;
11265
+        var origin;
11266
+        this.getEl(); // ensure this.el
11267
+        // make sure origin info was computed
11268
+        if (this.top0 == null) {
11269
+            sourceOffset = this.sourceEl.offset();
11270
+            origin = this.el.offsetParent().offset();
11271
+            this.top0 = sourceOffset.top - origin.top;
11272
+            this.left0 = sourceOffset.left - origin.left;
11273
         }
11274
-        return { duration: duration, unit: unit, unzonedRange: unzonedRange };
11275
+        this.el.css({
11276
+            top: this.top0 + this.topDelta,
11277
+            left: this.left0 + this.leftDelta
11278
+        });
11279
     };
11280
-    DateProfileGenerator.prototype.getFallbackDuration = function () {
11281
-        return moment.duration({ days: 1 });
11282
+    // Gets called when the user moves the mouse
11283
+    MouseFollower.prototype.handleMove = function (ev) {
11284
+        this.topDelta = util_1.getEvY(ev) - this.y0;
11285
+        this.leftDelta = util_1.getEvX(ev) - this.x0;
11286
+        if (!this.isHidden) {
11287
+            this.updatePosition();
11288
+        }
11289
     };
11290
-    // Returns a new activeUnzonedRange to have time values (un-ambiguate)
11291
-    // minTime or maxTime causes the range to expand.
11292
-    DateProfileGenerator.prototype.adjustActiveRange = function (unzonedRange, minTime, maxTime) {
11293
-        var start = unzonedRange.getStart();
11294
-        var end = unzonedRange.getEnd();
11295
-        if (this._view.usesMinMaxTime) {
11296
-            if (minTime < 0) {
11297
-                start.time(0).add(minTime);
11298
-            }
11299
-            if (maxTime > 24 * 60 * 60 * 1000) {
11300
-                end.time(maxTime - (24 * 60 * 60 * 1000));
11301
+    // Temporarily makes the tracking element invisible. Can be called before following starts
11302
+    MouseFollower.prototype.hide = function () {
11303
+        if (!this.isHidden) {
11304
+            this.isHidden = true;
11305
+            if (this.el) {
11306
+                this.el.hide();
11307
             }
11308
         }
11309
-        return new UnzonedRange_1.default(start, end);
11310
     };
11311
-    // Builds the "current" range when it is specified as an explicit duration.
11312
-    // `unit` is the already-computed computeGreatestUnit value of duration.
11313
-    // TODO: accept a MS-time instead of a moment `date`?
11314
-    DateProfileGenerator.prototype.buildRangeFromDuration = function (date, direction, duration, unit) {
11315
-        var alignment = this.opt('dateAlignment');
11316
-        var dateIncrementInput;
11317
-        var dateIncrementDuration;
11318
-        var start;
11319
-        var end;
11320
-        var res;
11321
-        // compute what the alignment should be
11322
-        if (!alignment) {
11323
-            dateIncrementInput = this.opt('dateIncrement');
11324
-            if (dateIncrementInput) {
11325
-                dateIncrementDuration = moment.duration(dateIncrementInput);
11326
-                // use the smaller of the two units
11327
-                if (dateIncrementDuration < duration) {
11328
-                    alignment = util_1.computeDurationGreatestUnit(dateIncrementDuration, dateIncrementInput);
11329
-                }
11330
-                else {
11331
-                    alignment = unit;
11332
-                }
11333
-            }
11334
-            else {
11335
-                alignment = unit;
11336
-            }
11337
+    // Show the tracking element after it has been temporarily hidden
11338
+    MouseFollower.prototype.show = function () {
11339
+        if (this.isHidden) {
11340
+            this.isHidden = false;
11341
+            this.updatePosition();
11342
+            this.getEl().show();
11343
         }
11344
-        // if the view displays a single day or smaller
11345
-        if (duration.as('days') <= 1) {
11346
-            if (this._view.isHiddenDay(start)) {
11347
-                start = this._view.skipHiddenDays(start, direction);
11348
-                start.startOf('day');
11349
+    };
11350
+    return MouseFollower;
11351
+}());
11352
+exports.default = MouseFollower;
11353
+ListenerMixin_1.default.mixInto(MouseFollower);
11354
+
11355
+
11356
+/***/ }),
11357
+/* 227 */
11358
+/***/ (function(module, exports, __webpack_require__) {
11359
+
11360
+/* A rectangular panel that is absolutely positioned over other content
11361
+------------------------------------------------------------------------------------------------------------------------
11362
+Options:
11363
+  - className (string)
11364
+  - content (HTML string or jQuery element set)
11365
+  - parentEl
11366
+  - top
11367
+  - left
11368
+  - right (the x coord of where the right edge should be. not a "CSS" right)
11369
+  - autoHide (boolean)
11370
+  - show (callback)
11371
+  - hide (callback)
11372
+*/
11373
+Object.defineProperty(exports, "__esModule", { value: true });
11374
+var $ = __webpack_require__(3);
11375
+var util_1 = __webpack_require__(4);
11376
+var ListenerMixin_1 = __webpack_require__(7);
11377
+var Popover = /** @class */ (function () {
11378
+    function Popover(options) {
11379
+        this.isHidden = true;
11380
+        this.margin = 10; // the space required between the popover and the edges of the scroll container
11381
+        this.options = options || {};
11382
+    }
11383
+    // Shows the popover on the specified position. Renders it if not already
11384
+    Popover.prototype.show = function () {
11385
+        if (this.isHidden) {
11386
+            if (!this.el) {
11387
+                this.render();
11388
             }
11389
+            this.el.show();
11390
+            this.position();
11391
+            this.isHidden = false;
11392
+            this.trigger('show');
11393
         }
11394
-        function computeRes() {
11395
-            start = date.clone().startOf(alignment);
11396
-            end = start.clone().add(duration);
11397
-            res = new UnzonedRange_1.default(start, end);
11398
-        }
11399
-        computeRes();
11400
-        // if range is completely enveloped by hidden days, go past the hidden days
11401
-        if (!this.trimHiddenDays(res)) {
11402
-            date = this._view.skipHiddenDays(date, direction);
11403
-            computeRes();
11404
+    };
11405
+    // Hides the popover, through CSS, but does not remove it from the DOM
11406
+    Popover.prototype.hide = function () {
11407
+        if (!this.isHidden) {
11408
+            this.el.hide();
11409
+            this.isHidden = true;
11410
+            this.trigger('hide');
11411
         }
11412
-        return res;
11413
     };
11414
-    // Builds the "current" range when a dayCount is specified.
11415
-    // TODO: accept a MS-time instead of a moment `date`?
11416
-    DateProfileGenerator.prototype.buildRangeFromDayCount = function (date, direction, dayCount) {
11417
-        var customAlignment = this.opt('dateAlignment');
11418
-        var runningCount = 0;
11419
-        var start = date.clone();
11420
-        var end;
11421
-        if (customAlignment) {
11422
-            start.startOf(customAlignment);
11423
+    // Creates `this.el` and renders content inside of it
11424
+    Popover.prototype.render = function () {
11425
+        var _this = this;
11426
+        var options = this.options;
11427
+        this.el = $('<div class="fc-popover">')
11428
+            .addClass(options.className || '')
11429
+            .css({
11430
+            // position initially to the top left to avoid creating scrollbars
11431
+            top: 0,
11432
+            left: 0
11433
+        })
11434
+            .append(options.content)
11435
+            .appendTo(options.parentEl);
11436
+        // when a click happens on anything inside with a 'fc-close' className, hide the popover
11437
+        this.el.on('click', '.fc-close', function () {
11438
+            _this.hide();
11439
+        });
11440
+        if (options.autoHide) {
11441
+            this.listenTo($(document), 'mousedown', this.documentMousedown);
11442
         }
11443
-        start.startOf('day');
11444
-        start = this._view.skipHiddenDays(start, direction);
11445
-        end = start.clone();
11446
-        do {
11447
-            end.add(1, 'day');
11448
-            if (!this._view.isHiddenDay(end)) {
11449
-                runningCount++;
11450
-            }
11451
-        } while (runningCount < dayCount);
11452
-        return new UnzonedRange_1.default(start, end);
11453
     };
11454
-    // Builds a normalized range object for the "visible" range,
11455
-    // which is a way to define the currentUnzonedRange and activeUnzonedRange at the same time.
11456
-    // TODO: accept a MS-time instead of a moment `date`?
11457
-    DateProfileGenerator.prototype.buildCustomVisibleRange = function (date) {
11458
-        var visibleUnzonedRange = this._view.getUnzonedRangeOption('visibleRange', this._view.calendar.applyTimezone(date) // correct zone. also generates new obj that avoids mutations
11459
-        );
11460
-        if (visibleUnzonedRange && (visibleUnzonedRange.startMs == null || visibleUnzonedRange.endMs == null)) {
11461
-            return null;
11462
+    // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
11463
+    Popover.prototype.documentMousedown = function (ev) {
11464
+        // only hide the popover if the click happened outside the popover
11465
+        if (this.el && !$(ev.target).closest(this.el).length) {
11466
+            this.hide();
11467
         }
11468
-        return visibleUnzonedRange;
11469
     };
11470
-    // Computes the range that will represent the element/cells for *rendering*,
11471
-    // but which may have voided days/times.
11472
-    // not responsible for trimming hidden days.
11473
-    DateProfileGenerator.prototype.buildRenderRange = function (currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
11474
-        return currentUnzonedRange.clone();
11475
+    // Hides and unregisters any handlers
11476
+    Popover.prototype.removeElement = function () {
11477
+        this.hide();
11478
+        if (this.el) {
11479
+            this.el.remove();
11480
+            this.el = null;
11481
+        }
11482
+        this.stopListeningTo($(document), 'mousedown');
11483
     };
11484
-    // Compute the duration value that should be added/substracted to the current date
11485
-    // when a prev/next operation happens.
11486
-    DateProfileGenerator.prototype.buildDateIncrement = function (fallback) {
11487
-        var dateIncrementInput = this.opt('dateIncrement');
11488
-        var customAlignment;
11489
-        if (dateIncrementInput) {
11490
-            return moment.duration(dateIncrementInput);
11491
+    // Positions the popover optimally, using the top/left/right options
11492
+    Popover.prototype.position = function () {
11493
+        var options = this.options;
11494
+        var origin = this.el.offsetParent().offset();
11495
+        var width = this.el.outerWidth();
11496
+        var height = this.el.outerHeight();
11497
+        var windowEl = $(window);
11498
+        var viewportEl = util_1.getScrollParent(this.el);
11499
+        var viewportTop;
11500
+        var viewportLeft;
11501
+        var viewportOffset;
11502
+        var top; // the "position" (not "offset") values for the popover
11503
+        var left; //
11504
+        // compute top and left
11505
+        top = options.top || 0;
11506
+        if (options.left !== undefined) {
11507
+            left = options.left;
11508
         }
11509
-        else if ((customAlignment = this.opt('dateAlignment'))) {
11510
-            return moment.duration(1, customAlignment);
11511
+        else if (options.right !== undefined) {
11512
+            left = options.right - width; // derive the left value from the right value
11513
         }
11514
-        else if (fallback) {
11515
-            return fallback;
11516
+        else {
11517
+            left = 0;
11518
+        }
11519
+        if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
11520
+            viewportEl = windowEl;
11521
+            viewportTop = 0; // the window is always at the top left
11522
+            viewportLeft = 0; // (and .offset() won't work if called here)
11523
         }
11524
         else {
11525
-            return moment.duration({ days: 1 });
11526
+            viewportOffset = viewportEl.offset();
11527
+            viewportTop = viewportOffset.top;
11528
+            viewportLeft = viewportOffset.left;
11529
+        }
11530
+        // if the window is scrolled, it causes the visible area to be further down
11531
+        viewportTop += windowEl.scrollTop();
11532
+        viewportLeft += windowEl.scrollLeft();
11533
+        // constrain to the view port. if constrained by two edges, give precedence to top/left
11534
+        if (options.viewportConstrain !== false) {
11535
+            top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
11536
+            top = Math.max(top, viewportTop + this.margin);
11537
+            left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
11538
+            left = Math.max(left, viewportLeft + this.margin);
11539
+        }
11540
+        this.el.css({
11541
+            top: top - origin.top,
11542
+            left: left - origin.left
11543
+        });
11544
+    };
11545
+    // Triggers a callback. Calls a function in the option hash of the same name.
11546
+    // Arguments beyond the first `name` are forwarded on.
11547
+    // TODO: better code reuse for this. Repeat code
11548
+    Popover.prototype.trigger = function (name) {
11549
+        if (this.options[name]) {
11550
+            this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
11551
         }
11552
     };
11553
-    return DateProfileGenerator;
11554
+    return Popover;
11555
 }());
11556
-exports.default = DateProfileGenerator;
11557
+exports.default = Popover;
11558
+ListenerMixin_1.default.mixInto(Popover);
11559
 
11560
 
11561
 /***/ }),
11562
-/* 222 */
11563
+/* 228 */
11564
 /***/ (function(module, exports, __webpack_require__) {
11565
 
11566
 Object.defineProperty(exports, "__esModule", { value: true });
11567
-var tslib_1 = __webpack_require__(2);
11568
-var $ = __webpack_require__(3);
11569
-var moment = __webpack_require__(0);
11570
-var exportHooks = __webpack_require__(16);
11571
-var util_1 = __webpack_require__(4);
11572
-var moment_ext_1 = __webpack_require__(10);
11573
-var ListenerMixin_1 = __webpack_require__(7);
11574
-var HitDragListener_1 = __webpack_require__(23);
11575
-var SingleEventDef_1 = __webpack_require__(13);
11576
-var EventInstanceGroup_1 = __webpack_require__(18);
11577
-var EventSource_1 = __webpack_require__(6);
11578
-var Interaction_1 = __webpack_require__(15);
11579
-var ExternalDropping = /** @class */ (function (_super) {
11580
-    tslib_1.__extends(ExternalDropping, _super);
11581
-    function ExternalDropping() {
11582
-        var _this = _super !== null && _super.apply(this, arguments) || this;
11583
-        _this.isDragging = false; // jqui-dragging an external element? boolean
11584
-        return _this;
11585
+var EmitterMixin_1 = __webpack_require__(13);
11586
+var TaskQueue = /** @class */ (function () {
11587
+    function TaskQueue() {
11588
+        this.q = [];
11589
+        this.isPaused = false;
11590
+        this.isRunning = false;
11591
     }
11592
-    /*
11593
-    component impements:
11594
-      - eventRangesToEventFootprints
11595
-      - isEventInstanceGroupAllowed
11596
-      - isExternalInstanceGroupAllowed
11597
-      - renderDrag
11598
-      - unrenderDrag
11599
-    */
11600
-    ExternalDropping.prototype.end = function () {
11601
-        if (this.dragListener) {
11602
-            this.dragListener.endInteraction();
11603
+    TaskQueue.prototype.queue = function () {
11604
+        var args = [];
11605
+        for (var _i = 0; _i < arguments.length; _i++) {
11606
+            args[_i] = arguments[_i];
11607
         }
11608
+        this.q.push.apply(this.q, args); // append
11609
+        this.tryStart();
11610
     };
11611
-    ExternalDropping.prototype.bindToDocument = function () {
11612
-        this.listenTo($(document), {
11613
-            dragstart: this.handleDragStart,
11614
-            sortstart: this.handleDragStart // jqui
11615
-        });
11616
+    TaskQueue.prototype.pause = function () {
11617
+        this.isPaused = true;
11618
     };
11619
-    ExternalDropping.prototype.unbindFromDocument = function () {
11620
-        this.stopListeningTo($(document));
11621
+    TaskQueue.prototype.resume = function () {
11622
+        this.isPaused = false;
11623
+        this.tryStart();
11624
     };
11625
-    // Called when a jQuery UI drag is initiated anywhere in the DOM
11626
-    ExternalDropping.prototype.handleDragStart = function (ev, ui) {
11627
-        var el;
11628
-        var accept;
11629
-        if (this.opt('droppable')) {
11630
-            el = $((ui ? ui.item : null) || ev.target);
11631
-            // Test that the dragged element passes the dropAccept selector or filter function.
11632
-            // FYI, the default is "*" (matches all)
11633
-            accept = this.opt('dropAccept');
11634
-            if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
11635
-                if (!this.isDragging) {
11636
-                    this.listenToExternalDrag(el, ev, ui);
11637
-                }
11638
-            }
11639
+    TaskQueue.prototype.getIsIdle = function () {
11640
+        return !this.isRunning && !this.isPaused;
11641
+    };
11642
+    TaskQueue.prototype.tryStart = function () {
11643
+        if (!this.isRunning && this.canRunNext()) {
11644
+            this.isRunning = true;
11645
+            this.trigger('start');
11646
+            this.runRemaining();
11647
         }
11648
     };
11649
-    // Called when a jQuery UI drag starts and it needs to be monitored for dropping
11650
-    ExternalDropping.prototype.listenToExternalDrag = function (el, ev, ui) {
11651
+    TaskQueue.prototype.canRunNext = function () {
11652
+        return !this.isPaused && this.q.length;
11653
+    };
11654
+    TaskQueue.prototype.runRemaining = function () {
11655
         var _this = this;
11656
-        var component = this.component;
11657
-        var view = this.view;
11658
-        var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
11659
-        var singleEventDef; // a null value signals an unsuccessful drag
11660
-        // listener that tracks mouse movement over date-associated pixel regions
11661
-        var dragListener = this.dragListener = new HitDragListener_1.default(component, {
11662
-            interactionStart: function () {
11663
-                _this.isDragging = true;
11664
-            },
11665
-            hitOver: function (hit) {
11666
-                var isAllowed = true;
11667
-                var hitFootprint = hit.component.getSafeHitFootprint(hit); // hit might not belong to this grid
11668
-                var mutatedEventInstanceGroup;
11669
-                if (hitFootprint) {
11670
-                    singleEventDef = _this.computeExternalDrop(hitFootprint, meta);
11671
-                    if (singleEventDef) {
11672
-                        mutatedEventInstanceGroup = new EventInstanceGroup_1.default(singleEventDef.buildInstances());
11673
-                        isAllowed = meta.eventProps ? // isEvent?
11674
-                            component.isEventInstanceGroupAllowed(mutatedEventInstanceGroup) :
11675
-                            component.isExternalInstanceGroupAllowed(mutatedEventInstanceGroup);
11676
-                    }
11677
-                    else {
11678
-                        isAllowed = false;
11679
+        var task;
11680
+        var res;
11681
+        do {
11682
+            task = this.q.shift(); // always freshly reference q. might have been reassigned.
11683
+            res = this.runTask(task);
11684
+            if (res && res.then) {
11685
+                res.then(function () {
11686
+                    if (_this.canRunNext()) {
11687
+                        _this.runRemaining();
11688
                     }
11689
-                }
11690
-                else {
11691
-                    isAllowed = false;
11692
-                }
11693
-                if (!isAllowed) {
11694
-                    singleEventDef = null;
11695
-                    util_1.disableCursor();
11696
-                }
11697
-                if (singleEventDef) {
11698
-                    component.renderDrag(// called without a seg parameter
11699
-                    component.eventRangesToEventFootprints(mutatedEventInstanceGroup.sliceRenderRanges(component.dateProfile.renderUnzonedRange, view.calendar)));
11700
-                }
11701
-            },
11702
-            hitOut: function () {
11703
-                singleEventDef = null; // signal unsuccessful
11704
-            },
11705
-            hitDone: function () {
11706
-                util_1.enableCursor();
11707
-                component.unrenderDrag();
11708
-            },
11709
-            interactionEnd: function (ev) {
11710
-                if (singleEventDef) {
11711
-                    view.reportExternalDrop(singleEventDef, Boolean(meta.eventProps), // isEvent
11712
-                    Boolean(meta.stick), // isSticky
11713
-                    el, ev, ui);
11714
-                }
11715
-                _this.isDragging = false;
11716
-                _this.dragListener = null;
11717
+                });
11718
+                return; // prevent marking as stopped
11719
             }
11720
-        });
11721
-        dragListener.startDrag(ev); // start listening immediately
11722
+        } while (this.canRunNext());
11723
+        this.trigger('stop'); // not really a 'stop' ... more of a 'drained'
11724
+        this.isRunning = false;
11725
+        // if 'stop' handler added more tasks.... TODO: write test for this
11726
+        this.tryStart();
11727
     };
11728
-    // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
11729
-    // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
11730
-    // Returning a null value signals an invalid drop hit.
11731
-    // DOES NOT consider overlap/constraint.
11732
-    // Assumes both footprints are non-open-ended.
11733
-    ExternalDropping.prototype.computeExternalDrop = function (componentFootprint, meta) {
11734
-        var calendar = this.view.calendar;
11735
-        var start = moment_ext_1.default.utc(componentFootprint.unzonedRange.startMs).stripZone();
11736
-        var end;
11737
-        var eventDef;
11738
-        if (componentFootprint.isAllDay) {
11739
-            // if dropped on an all-day span, and element's metadata specified a time, set it
11740
-            if (meta.startTime) {
11741
-                start.time(meta.startTime);
11742
+    TaskQueue.prototype.runTask = function (task) {
11743
+        return task(); // task *is* the function, but subclasses can change the format of a task
11744
+    };
11745
+    return TaskQueue;
11746
+}());
11747
+exports.default = TaskQueue;
11748
+EmitterMixin_1.default.mixInto(TaskQueue);
11749
+
11750
+
11751
+/***/ }),
11752
+/* 229 */
11753
+/***/ (function(module, exports, __webpack_require__) {
11754
+
11755
+Object.defineProperty(exports, "__esModule", { value: true });
11756
+var tslib_1 = __webpack_require__(2);
11757
+var TaskQueue_1 = __webpack_require__(228);
11758
+var RenderQueue = /** @class */ (function (_super) {
11759
+    tslib_1.__extends(RenderQueue, _super);
11760
+    function RenderQueue(waitsByNamespace) {
11761
+        var _this = _super.call(this) || this;
11762
+        _this.waitsByNamespace = waitsByNamespace || {};
11763
+        return _this;
11764
+    }
11765
+    RenderQueue.prototype.queue = function (taskFunc, namespace, type) {
11766
+        var task = {
11767
+            func: taskFunc,
11768
+            namespace: namespace,
11769
+            type: type
11770
+        };
11771
+        var waitMs;
11772
+        if (namespace) {
11773
+            waitMs = this.waitsByNamespace[namespace];
11774
+        }
11775
+        if (this.waitNamespace) {
11776
+            if (namespace === this.waitNamespace && waitMs != null) {
11777
+                this.delayWait(waitMs);
11778
             }
11779
             else {
11780
-                start.stripTime();
11781
+                this.clearWait();
11782
+                this.tryStart();
11783
             }
11784
         }
11785
-        if (meta.duration) {
11786
-            end = start.clone().add(meta.duration);
11787
+        if (this.compoundTask(task)) { // appended to queue?
11788
+            if (!this.waitNamespace && waitMs != null) {
11789
+                this.startWait(namespace, waitMs);
11790
+            }
11791
+            else {
11792
+                this.tryStart();
11793
+            }
11794
         }
11795
-        start = calendar.applyTimezone(start);
11796
-        if (end) {
11797
-            end = calendar.applyTimezone(end);
11798
+    };
11799
+    RenderQueue.prototype.startWait = function (namespace, waitMs) {
11800
+        this.waitNamespace = namespace;
11801
+        this.spawnWait(waitMs);
11802
+    };
11803
+    RenderQueue.prototype.delayWait = function (waitMs) {
11804
+        clearTimeout(this.waitId);
11805
+        this.spawnWait(waitMs);
11806
+    };
11807
+    RenderQueue.prototype.spawnWait = function (waitMs) {
11808
+        var _this = this;
11809
+        this.waitId = setTimeout(function () {
11810
+            _this.waitNamespace = null;
11811
+            _this.tryStart();
11812
+        }, waitMs);
11813
+    };
11814
+    RenderQueue.prototype.clearWait = function () {
11815
+        if (this.waitNamespace) {
11816
+            clearTimeout(this.waitId);
11817
+            this.waitId = null;
11818
+            this.waitNamespace = null;
11819
+        }
11820
+    };
11821
+    RenderQueue.prototype.canRunNext = function () {
11822
+        if (!_super.prototype.canRunNext.call(this)) {
11823
+            return false;
11824
+        }
11825
+        // waiting for a certain namespace to stop receiving tasks?
11826
+        if (this.waitNamespace) {
11827
+            var q = this.q;
11828
+            // if there was a different namespace task in the meantime,
11829
+            // that forces all previously-waiting tasks to suddenly execute.
11830
+            // TODO: find a way to do this in constant time.
11831
+            for (var i = 0; i < q.length; i++) {
11832
+                if (q[i].namespace !== this.waitNamespace) {
11833
+                    return true; // allow execution
11834
+                }
11835
+            }
11836
+            return false;
11837
         }
11838
-        eventDef = SingleEventDef_1.default.parse($.extend({}, meta.eventProps, {
11839
-            start: start,
11840
-            end: end
11841
-        }), new EventSource_1.default(calendar));
11842
-        return eventDef;
11843
+        return true;
11844
     };
11845
-    return ExternalDropping;
11846
-}(Interaction_1.default));
11847
-exports.default = ExternalDropping;
11848
-ListenerMixin_1.default.mixInto(ExternalDropping);
11849
-/* External-Dragging-Element Data
11850
-----------------------------------------------------------------------------------------------------------------------*/
11851
-// Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
11852
-// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
11853
-exportHooks.dataAttrPrefix = '';
11854
-// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
11855
-// to be used for Event Object creation.
11856
-// A defined `.eventProps`, even when empty, indicates that an event should be created.
11857
-function getDraggedElMeta(el) {
11858
-    var prefix = exportHooks.dataAttrPrefix;
11859
-    var eventProps; // properties for creating the event, not related to date/time
11860
-    var startTime; // a Duration
11861
-    var duration;
11862
-    var stick;
11863
-    if (prefix) {
11864
-        prefix += '-';
11865
-    }
11866
-    eventProps = el.data(prefix + 'event') || null;
11867
-    if (eventProps) {
11868
-        if (typeof eventProps === 'object') {
11869
-            eventProps = $.extend({}, eventProps); // make a copy
11870
+    RenderQueue.prototype.runTask = function (task) {
11871
+        task.func();
11872
+    };
11873
+    RenderQueue.prototype.compoundTask = function (newTask) {
11874
+        var q = this.q;
11875
+        var shouldAppend = true;
11876
+        var i;
11877
+        var task;
11878
+        if (newTask.namespace && newTask.type === 'destroy') {
11879
+            // remove all init/add/remove ops with same namespace, regardless of order
11880
+            for (i = q.length - 1; i >= 0; i--) {
11881
+                task = q[i];
11882
+                if (task.namespace === newTask.namespace) {
11883
+                    switch (task.type) {
11884
+                        case 'init':
11885
+                            shouldAppend = false;
11886
+                        // the latest destroy is cancelled out by not doing the init
11887
+                        /* falls through */
11888
+                        case 'add':
11889
+                        /* falls through */
11890
+                        case 'remove':
11891
+                            q.splice(i, 1); // remove task
11892
+                    }
11893
+                }
11894
+            }
11895
         }
11896
-        else {
11897
-            eventProps = {};
11898
+        if (shouldAppend) {
11899
+            q.push(newTask);
11900
         }
11901
-        // pluck special-cased date/time properties
11902
-        startTime = eventProps.start;
11903
-        if (startTime == null) {
11904
-            startTime = eventProps.time;
11905
-        } // accept 'time' as well
11906
-        duration = eventProps.duration;
11907
-        stick = eventProps.stick;
11908
-        delete eventProps.start;
11909
-        delete eventProps.time;
11910
-        delete eventProps.duration;
11911
-        delete eventProps.stick;
11912
-    }
11913
-    // fallback to standalone attribute values for each of the date/time properties
11914
-    if (startTime == null) {
11915
-        startTime = el.data(prefix + 'start');
11916
-    }
11917
-    if (startTime == null) {
11918
-        startTime = el.data(prefix + 'time');
11919
-    } // accept 'time' as well
11920
-    if (duration == null) {
11921
-        duration = el.data(prefix + 'duration');
11922
-    }
11923
-    if (stick == null) {
11924
-        stick = el.data(prefix + 'stick');
11925
+        return shouldAppend;
11926
+    };
11927
+    return RenderQueue;
11928
+}(TaskQueue_1.default));
11929
+exports.default = RenderQueue;
11930
+
11931
+
11932
+/***/ }),
11933
+/* 230 */
11934
+/***/ (function(module, exports, __webpack_require__) {
11935
+
11936
+Object.defineProperty(exports, "__esModule", { value: true });
11937
+var tslib_1 = __webpack_require__(2);
11938
+var Model_1 = __webpack_require__(51);
11939
+var Component = /** @class */ (function (_super) {
11940
+    tslib_1.__extends(Component, _super);
11941
+    function Component() {
11942
+        return _super !== null && _super.apply(this, arguments) || this;
11943
     }
11944
-    // massage into correct data types
11945
-    startTime = startTime != null ? moment.duration(startTime) : null;
11946
-    duration = duration != null ? moment.duration(duration) : null;
11947
-    stick = Boolean(stick);
11948
-    return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
11949
-}
11950
+    Component.prototype.setElement = function (el) {
11951
+        this.el = el;
11952
+        this.bindGlobalHandlers();
11953
+        this.renderSkeleton();
11954
+        this.set('isInDom', true);
11955
+    };
11956
+    Component.prototype.removeElement = function () {
11957
+        this.unset('isInDom');
11958
+        this.unrenderSkeleton();
11959
+        this.unbindGlobalHandlers();
11960
+        this.el.remove();
11961
+        // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
11962
+        // We don't null-out the View's other jQuery element references upon destroy,
11963
+        //  so we shouldn't kill this.el either.
11964
+    };
11965
+    Component.prototype.bindGlobalHandlers = function () {
11966
+        // subclasses can override
11967
+    };
11968
+    Component.prototype.unbindGlobalHandlers = function () {
11969
+        // subclasses can override
11970
+    };
11971
+    /*
11972
+    NOTE: Can't have a `render` method. Read the deprecation notice in View::executeDateRender
11973
+    */
11974
+    // Renders the basic structure of the view before any content is rendered
11975
+    Component.prototype.renderSkeleton = function () {
11976
+        // subclasses should implement
11977
+    };
11978
+    // Unrenders the basic structure of the view
11979
+    Component.prototype.unrenderSkeleton = function () {
11980
+        // subclasses should implement
11981
+    };
11982
+    return Component;
11983
+}(Model_1.default));
11984
+exports.default = Component;
11985
 
11986
 
11987
 /***/ }),
11988
-/* 223 */
11989
+/* 231 */
11990
 /***/ (function(module, exports, __webpack_require__) {
11991
 
11992
 Object.defineProperty(exports, "__esModule", { value: true });
11993
 var tslib_1 = __webpack_require__(2);
11994
 var $ = __webpack_require__(3);
11995
+var moment = __webpack_require__(0);
11996
 var util_1 = __webpack_require__(4);
11997
-var EventDefMutation_1 = __webpack_require__(37);
11998
-var EventDefDateMutation_1 = __webpack_require__(50);
11999
-var HitDragListener_1 = __webpack_require__(23);
12000
-var Interaction_1 = __webpack_require__(15);
12001
-var EventResizing = /** @class */ (function (_super) {
12002
-    tslib_1.__extends(EventResizing, _super);
12003
-    /*
12004
-    component impements:
12005
-      - bindSegHandlerToEl
12006
-      - publiclyTrigger
12007
-      - diffDates
12008
-      - eventRangesToEventFootprints
12009
-      - isEventInstanceGroupAllowed
12010
-      - getSafeHitFootprint
12011
-    */
12012
-    function EventResizing(component, eventPointing) {
12013
-        var _this = _super.call(this, component) || this;
12014
-        _this.isResizing = false;
12015
-        _this.eventPointing = eventPointing;
12016
+var moment_ext_1 = __webpack_require__(11);
12017
+var date_formatting_1 = __webpack_require__(49);
12018
+var Component_1 = __webpack_require__(230);
12019
+var util_2 = __webpack_require__(19);
12020
+var DateComponent = /** @class */ (function (_super) {
12021
+    tslib_1.__extends(DateComponent, _super);
12022
+    function DateComponent(_view, _options) {
12023
+        var _this = _super.call(this) || this;
12024
+        _this.isRTL = false; // frequently accessed options
12025
+        _this.hitsNeededDepth = 0; // necessary because multiple callers might need the same hits
12026
+        _this.hasAllDayBusinessHours = false; // TODO: unify with largeUnit and isTimeScale?
12027
+        _this.isDatesRendered = false;
12028
+        // hack to set options prior to the this.opt calls
12029
+        if (_view) {
12030
+            _this['view'] = _view;
12031
+        }
12032
+        if (_options) {
12033
+            _this['options'] = _options;
12034
+        }
12035
+        _this.uid = String(DateComponent.guid++);
12036
+        _this.childrenByUid = {};
12037
+        _this.nextDayThreshold = moment.duration(_this.opt('nextDayThreshold'));
12038
+        _this.isRTL = _this.opt('isRTL');
12039
+        if (_this.fillRendererClass) {
12040
+            _this.fillRenderer = new _this.fillRendererClass(_this);
12041
+        }
12042
+        if (_this.eventRendererClass) { // fillRenderer is optional -----v
12043
+            _this.eventRenderer = new _this.eventRendererClass(_this, _this.fillRenderer);
12044
+        }
12045
+        if (_this.helperRendererClass && _this.eventRenderer) {
12046
+            _this.helperRenderer = new _this.helperRendererClass(_this, _this.eventRenderer);
12047
+        }
12048
+        if (_this.businessHourRendererClass && _this.fillRenderer) {
12049
+            _this.businessHourRenderer = new _this.businessHourRendererClass(_this, _this.fillRenderer);
12050
+        }
12051
         return _this;
12052
     }
12053
-    EventResizing.prototype.end = function () {
12054
-        if (this.dragListener) {
12055
-            this.dragListener.endInteraction();
12056
+    DateComponent.prototype.addChild = function (child) {
12057
+        if (!this.childrenByUid[child.uid]) {
12058
+            this.childrenByUid[child.uid] = child;
12059
+            return true;
12060
+        }
12061
+        return false;
12062
+    };
12063
+    DateComponent.prototype.removeChild = function (child) {
12064
+        if (this.childrenByUid[child.uid]) {
12065
+            delete this.childrenByUid[child.uid];
12066
+            return true;
12067
+        }
12068
+        return false;
12069
+    };
12070
+    // TODO: only do if isInDom?
12071
+    // TODO: make part of Component, along with children/batch-render system?
12072
+    DateComponent.prototype.updateSize = function (totalHeight, isAuto, isResize) {
12073
+        this.callChildren('updateSize', arguments);
12074
+    };
12075
+    // Options
12076
+    // -----------------------------------------------------------------------------------------------------------------
12077
+    DateComponent.prototype.opt = function (name) {
12078
+        return this._getView().opt(name); // default implementation
12079
+    };
12080
+    DateComponent.prototype.publiclyTrigger = function () {
12081
+        var args = [];
12082
+        for (var _i = 0; _i < arguments.length; _i++) {
12083
+            args[_i] = arguments[_i];
12084
+        }
12085
+        var calendar = this._getCalendar();
12086
+        return calendar.publiclyTrigger.apply(calendar, args);
12087
+    };
12088
+    DateComponent.prototype.hasPublicHandlers = function () {
12089
+        var args = [];
12090
+        for (var _i = 0; _i < arguments.length; _i++) {
12091
+            args[_i] = arguments[_i];
12092
+        }
12093
+        var calendar = this._getCalendar();
12094
+        return calendar.hasPublicHandlers.apply(calendar, args);
12095
+    };
12096
+    // Date
12097
+    // -----------------------------------------------------------------------------------------------------------------
12098
+    DateComponent.prototype.executeDateRender = function (dateProfile) {
12099
+        this.dateProfile = dateProfile; // for rendering
12100
+        this.renderDates(dateProfile);
12101
+        this.isDatesRendered = true;
12102
+        this.callChildren('executeDateRender', arguments);
12103
+    };
12104
+    DateComponent.prototype.executeDateUnrender = function () {
12105
+        this.callChildren('executeDateUnrender', arguments);
12106
+        this.dateProfile = null;
12107
+        this.unrenderDates();
12108
+        this.isDatesRendered = false;
12109
+    };
12110
+    // date-cell content only
12111
+    DateComponent.prototype.renderDates = function (dateProfile) {
12112
+        // subclasses should implement
12113
+    };
12114
+    // date-cell content only
12115
+    DateComponent.prototype.unrenderDates = function () {
12116
+        // subclasses should override
12117
+    };
12118
+    // Now-Indicator
12119
+    // -----------------------------------------------------------------------------------------------------------------
12120
+    // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
12121
+    // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
12122
+    DateComponent.prototype.getNowIndicatorUnit = function () {
12123
+        // subclasses should implement
12124
+    };
12125
+    // Renders a current time indicator at the given datetime
12126
+    DateComponent.prototype.renderNowIndicator = function (date) {
12127
+        this.callChildren('renderNowIndicator', arguments);
12128
+    };
12129
+    // Undoes the rendering actions from renderNowIndicator
12130
+    DateComponent.prototype.unrenderNowIndicator = function () {
12131
+        this.callChildren('unrenderNowIndicator', arguments);
12132
+    };
12133
+    // Business Hours
12134
+    // ---------------------------------------------------------------------------------------------------------------
12135
+    DateComponent.prototype.renderBusinessHours = function (businessHourGenerator) {
12136
+        if (this.businessHourRenderer) {
12137
+            this.businessHourRenderer.render(businessHourGenerator);
12138
         }
12139
+        this.callChildren('renderBusinessHours', arguments);
12140
     };
12141
-    EventResizing.prototype.bindToEl = function (el) {
12142
-        var component = this.component;
12143
-        component.bindSegHandlerToEl(el, 'mousedown', this.handleMouseDown.bind(this));
12144
-        component.bindSegHandlerToEl(el, 'touchstart', this.handleTouchStart.bind(this));
12145
+    // Unrenders previously-rendered business-hours
12146
+    DateComponent.prototype.unrenderBusinessHours = function () {
12147
+        this.callChildren('unrenderBusinessHours', arguments);
12148
+        if (this.businessHourRenderer) {
12149
+            this.businessHourRenderer.unrender();
12150
+        }
12151
     };
12152
-    EventResizing.prototype.handleMouseDown = function (seg, ev) {
12153
-        if (this.component.canStartResize(seg, ev)) {
12154
-            this.buildDragListener(seg, $(ev.target).is('.fc-start-resizer'))
12155
-                .startInteraction(ev, { distance: 5 });
12156
+    // Event Displaying
12157
+    // -----------------------------------------------------------------------------------------------------------------
12158
+    DateComponent.prototype.executeEventRender = function (eventsPayload) {
12159
+        if (this.eventRenderer) {
12160
+            this.eventRenderer.rangeUpdated(); // poorly named now
12161
+            this.eventRenderer.render(eventsPayload);
12162
+        }
12163
+        else if (this['renderEvents']) { // legacy
12164
+            this['renderEvents'](convertEventsPayloadToLegacyArray(eventsPayload));
12165
         }
12166
+        this.callChildren('executeEventRender', arguments);
12167
     };
12168
-    EventResizing.prototype.handleTouchStart = function (seg, ev) {
12169
-        if (this.component.canStartResize(seg, ev)) {
12170
-            this.buildDragListener(seg, $(ev.target).is('.fc-start-resizer'))
12171
-                .startInteraction(ev);
12172
+    DateComponent.prototype.executeEventUnrender = function () {
12173
+        this.callChildren('executeEventUnrender', arguments);
12174
+        if (this.eventRenderer) {
12175
+            this.eventRenderer.unrender();
12176
+        }
12177
+        else if (this['destroyEvents']) { // legacy
12178
+            this['destroyEvents']();
12179
         }
12180
     };
12181
-    // Creates a listener that tracks the user as they resize an event segment.
12182
-    // Generic enough to work with any type of Grid.
12183
-    EventResizing.prototype.buildDragListener = function (seg, isStart) {
12184
+    DateComponent.prototype.getBusinessHourSegs = function () {
12185
+        var segs = this.getOwnBusinessHourSegs();
12186
+        this.iterChildren(function (child) {
12187
+            segs.push.apply(segs, child.getBusinessHourSegs());
12188
+        });
12189
+        return segs;
12190
+    };
12191
+    DateComponent.prototype.getOwnBusinessHourSegs = function () {
12192
+        if (this.businessHourRenderer) {
12193
+            return this.businessHourRenderer.getSegs();
12194
+        }
12195
+        return [];
12196
+    };
12197
+    DateComponent.prototype.getEventSegs = function () {
12198
+        var segs = this.getOwnEventSegs();
12199
+        this.iterChildren(function (child) {
12200
+            segs.push.apply(segs, child.getEventSegs());
12201
+        });
12202
+        return segs;
12203
+    };
12204
+    DateComponent.prototype.getOwnEventSegs = function () {
12205
+        if (this.eventRenderer) {
12206
+            return this.eventRenderer.getSegs();
12207
+        }
12208
+        return [];
12209
+    };
12210
+    // Event Rendering Triggering
12211
+    // -----------------------------------------------------------------------------------------------------------------
12212
+    DateComponent.prototype.triggerAfterEventsRendered = function () {
12213
+        this.triggerAfterEventSegsRendered(this.getEventSegs());
12214
+        this.publiclyTrigger('eventAfterAllRender', {
12215
+            context: this,
12216
+            args: [this]
12217
+        });
12218
+    };
12219
+    DateComponent.prototype.triggerAfterEventSegsRendered = function (segs) {
12220
         var _this = this;
12221
-        var component = this.component;
12222
-        var view = this.view;
12223
-        var calendar = view.calendar;
12224
-        var eventManager = calendar.eventManager;
12225
-        var el = seg.el;
12226
-        var eventDef = seg.footprint.eventDef;
12227
-        var eventInstance = seg.footprint.eventInstance;
12228
-        var isDragging;
12229
-        var resizeMutation; // zoned event date properties. falsy if invalid resize
12230
-        // Tracks mouse movement over the *grid's* coordinate map
12231
-        var dragListener = this.dragListener = new HitDragListener_1.default(component, {
12232
-            scroll: this.opt('dragScroll'),
12233
-            subjectEl: el,
12234
-            interactionStart: function () {
12235
-                isDragging = false;
12236
-            },
12237
-            dragStart: function (ev) {
12238
-                isDragging = true;
12239
-                // ensure a mouseout on the manipulated event has been reported
12240
-                _this.eventPointing.handleMouseout(seg, ev);
12241
-                _this.segResizeStart(seg, ev);
12242
-            },
12243
-            hitOver: function (hit, isOrig, origHit) {
12244
-                var isAllowed = true;
12245
-                var origHitFootprint = component.getSafeHitFootprint(origHit);
12246
-                var hitFootprint = component.getSafeHitFootprint(hit);
12247
-                var mutatedEventInstanceGroup;
12248
-                if (origHitFootprint && hitFootprint) {
12249
-                    resizeMutation = isStart ?
12250
-                        _this.computeEventStartResizeMutation(origHitFootprint, hitFootprint, seg.footprint) :
12251
-                        _this.computeEventEndResizeMutation(origHitFootprint, hitFootprint, seg.footprint);
12252
-                    if (resizeMutation) {
12253
-                        mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(eventDef.id, resizeMutation);
12254
-                        isAllowed = component.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
12255
-                    }
12256
-                    else {
12257
-                        isAllowed = false;
12258
-                    }
12259
-                }
12260
-                else {
12261
-                    isAllowed = false;
12262
-                }
12263
-                if (!isAllowed) {
12264
-                    resizeMutation = null;
12265
-                    util_1.disableCursor();
12266
-                }
12267
-                else if (resizeMutation.isEmpty()) {
12268
-                    // no change. (FYI, event dates might have zones)
12269
-                    resizeMutation = null;
12270
-                }
12271
-                if (resizeMutation) {
12272
-                    view.hideEventsWithId(seg.footprint.eventDef.id);
12273
-                    view.renderEventResize(component.eventRangesToEventFootprints(mutatedEventInstanceGroup.sliceRenderRanges(component.dateProfile.renderUnzonedRange, calendar)), seg);
12274
+        // an optimization, because getEventLegacy is expensive
12275
+        if (this.hasPublicHandlers('eventAfterRender')) {
12276
+            segs.forEach(function (seg) {
12277
+                var legacy;
12278
+                if (seg.el) { // necessary?
12279
+                    legacy = seg.footprint.getEventLegacy();
12280
+                    _this.publiclyTrigger('eventAfterRender', {
12281
+                        context: legacy,
12282
+                        args: [legacy, seg.el, _this]
12283
+                    });
12284
                 }
12285
-            },
12286
-            hitOut: function () {
12287
-                resizeMutation = null;
12288
-            },
12289
-            hitDone: function () {
12290
-                view.unrenderEventResize(seg);
12291
-                view.showEventsWithId(seg.footprint.eventDef.id);
12292
-                util_1.enableCursor();
12293
-            },
12294
-            interactionEnd: function (ev) {
12295
-                if (isDragging) {
12296
-                    _this.segResizeStop(seg, ev);
12297
+            });
12298
+        }
12299
+    };
12300
+    DateComponent.prototype.triggerBeforeEventsDestroyed = function () {
12301
+        this.triggerBeforeEventSegsDestroyed(this.getEventSegs());
12302
+    };
12303
+    DateComponent.prototype.triggerBeforeEventSegsDestroyed = function (segs) {
12304
+        var _this = this;
12305
+        if (this.hasPublicHandlers('eventDestroy')) {
12306
+            segs.forEach(function (seg) {
12307
+                var legacy;
12308
+                if (seg.el) { // necessary?
12309
+                    legacy = seg.footprint.getEventLegacy();
12310
+                    _this.publiclyTrigger('eventDestroy', {
12311
+                        context: legacy,
12312
+                        args: [legacy, seg.el, _this]
12313
+                    });
12314
                 }
12315
-                if (resizeMutation) {
12316
-                    // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
12317
-                    view.reportEventResize(eventInstance, resizeMutation, el, ev);
12318
+            });
12319
+        }
12320
+    };
12321
+    // Event Rendering Utils
12322
+    // -----------------------------------------------------------------------------------------------------------------
12323
+    // Hides all rendered event segments linked to the given event
12324
+    // RECURSIVE with subcomponents
12325
+    DateComponent.prototype.showEventsWithId = function (eventDefId) {
12326
+        this.getEventSegs().forEach(function (seg) {
12327
+            if (seg.footprint.eventDef.id === eventDefId &&
12328
+                seg.el // necessary?
12329
+            ) {
12330
+                seg.el.css('visibility', '');
12331
+            }
12332
+        });
12333
+        this.callChildren('showEventsWithId', arguments);
12334
+    };
12335
+    // Shows all rendered event segments linked to the given event
12336
+    // RECURSIVE with subcomponents
12337
+    DateComponent.prototype.hideEventsWithId = function (eventDefId) {
12338
+        this.getEventSegs().forEach(function (seg) {
12339
+            if (seg.footprint.eventDef.id === eventDefId &&
12340
+                seg.el // necessary?
12341
+            ) {
12342
+                seg.el.css('visibility', 'hidden');
12343
+            }
12344
+        });
12345
+        this.callChildren('hideEventsWithId', arguments);
12346
+    };
12347
+    // Drag-n-Drop Rendering (for both events and external elements)
12348
+    // ---------------------------------------------------------------------------------------------------------------
12349
+    // Renders a visual indication of a event or external-element drag over the given drop zone.
12350
+    // If an external-element, seg will be `null`.
12351
+    // Must return elements used for any mock events.
12352
+    DateComponent.prototype.renderDrag = function (eventFootprints, seg, isTouch) {
12353
+        var renderedHelper = false;
12354
+        this.iterChildren(function (child) {
12355
+            if (child.renderDrag(eventFootprints, seg, isTouch)) {
12356
+                renderedHelper = true;
12357
+            }
12358
+        });
12359
+        return renderedHelper;
12360
+    };
12361
+    // Unrenders a visual indication of an event or external-element being dragged.
12362
+    DateComponent.prototype.unrenderDrag = function () {
12363
+        this.callChildren('unrenderDrag', arguments);
12364
+    };
12365
+    // Event Resizing
12366
+    // ---------------------------------------------------------------------------------------------------------------
12367
+    // Renders a visual indication of an event being resized.
12368
+    DateComponent.prototype.renderEventResize = function (eventFootprints, seg, isTouch) {
12369
+        this.callChildren('renderEventResize', arguments);
12370
+    };
12371
+    // Unrenders a visual indication of an event being resized.
12372
+    DateComponent.prototype.unrenderEventResize = function () {
12373
+        this.callChildren('unrenderEventResize', arguments);
12374
+    };
12375
+    // Selection
12376
+    // ---------------------------------------------------------------------------------------------------------------
12377
+    // Renders a visual indication of the selection
12378
+    // TODO: rename to `renderSelection` after legacy is gone
12379
+    DateComponent.prototype.renderSelectionFootprint = function (componentFootprint) {
12380
+        this.renderHighlight(componentFootprint);
12381
+        this.callChildren('renderSelectionFootprint', arguments);
12382
+    };
12383
+    // Unrenders a visual indication of selection
12384
+    DateComponent.prototype.unrenderSelection = function () {
12385
+        this.unrenderHighlight();
12386
+        this.callChildren('unrenderSelection', arguments);
12387
+    };
12388
+    // Highlight
12389
+    // ---------------------------------------------------------------------------------------------------------------
12390
+    // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
12391
+    DateComponent.prototype.renderHighlight = function (componentFootprint) {
12392
+        if (this.fillRenderer) {
12393
+            this.fillRenderer.renderFootprint('highlight', componentFootprint, {
12394
+                getClasses: function () {
12395
+                    return ['fc-highlight'];
12396
                 }
12397
-                _this.dragListener = null;
12398
-            }
12399
-        });
12400
-        return dragListener;
12401
+            });
12402
+        }
12403
+        this.callChildren('renderHighlight', arguments);
12404
     };
12405
-    // Called before event segment resizing starts
12406
-    EventResizing.prototype.segResizeStart = function (seg, ev) {
12407
-        this.isResizing = true;
12408
-        this.component.publiclyTrigger('eventResizeStart', {
12409
-            context: seg.el[0],
12410
-            args: [
12411
-                seg.footprint.getEventLegacy(),
12412
-                ev,
12413
-                {},
12414
-                this.view
12415
-            ]
12416
-        });
12417
+    // Unrenders the emphasis on a date range
12418
+    DateComponent.prototype.unrenderHighlight = function () {
12419
+        if (this.fillRenderer) {
12420
+            this.fillRenderer.unrender('highlight');
12421
+        }
12422
+        this.callChildren('unrenderHighlight', arguments);
12423
     };
12424
-    // Called after event segment resizing stops
12425
-    EventResizing.prototype.segResizeStop = function (seg, ev) {
12426
-        this.isResizing = false;
12427
-        this.component.publiclyTrigger('eventResizeStop', {
12428
-            context: seg.el[0],
12429
-            args: [
12430
-                seg.footprint.getEventLegacy(),
12431
-                ev,
12432
-                {},
12433
-                this.view
12434
-            ]
12435
-        });
12436
+    // Hit Areas
12437
+    // ---------------------------------------------------------------------------------------------------------------
12438
+    // just because all DateComponents support this interface
12439
+    // doesn't mean they need to have their own internal coord system. they can defer to sub-components.
12440
+    DateComponent.prototype.hitsNeeded = function () {
12441
+        if (!(this.hitsNeededDepth++)) {
12442
+            this.prepareHits();
12443
+        }
12444
+        this.callChildren('hitsNeeded', arguments);
12445
     };
12446
-    // Returns new date-information for an event segment being resized from its start
12447
-    EventResizing.prototype.computeEventStartResizeMutation = function (startFootprint, endFootprint, origEventFootprint) {
12448
-        var origRange = origEventFootprint.componentFootprint.unzonedRange;
12449
-        var startDelta = this.component.diffDates(endFootprint.unzonedRange.getStart(), startFootprint.unzonedRange.getStart());
12450
-        var dateMutation;
12451
-        var eventDefMutation;
12452
-        if (origRange.getStart().add(startDelta) < origRange.getEnd()) {
12453
-            dateMutation = new EventDefDateMutation_1.default();
12454
-            dateMutation.setStartDelta(startDelta);
12455
-            eventDefMutation = new EventDefMutation_1.default();
12456
-            eventDefMutation.setDateMutation(dateMutation);
12457
-            return eventDefMutation;
12458
+    DateComponent.prototype.hitsNotNeeded = function () {
12459
+        if (this.hitsNeededDepth && !(--this.hitsNeededDepth)) {
12460
+            this.releaseHits();
12461
         }
12462
-        return false;
12463
+        this.callChildren('hitsNotNeeded', arguments);
12464
     };
12465
-    // Returns new date-information for an event segment being resized from its end
12466
-    EventResizing.prototype.computeEventEndResizeMutation = function (startFootprint, endFootprint, origEventFootprint) {
12467
-        var origRange = origEventFootprint.componentFootprint.unzonedRange;
12468
-        var endDelta = this.component.diffDates(endFootprint.unzonedRange.getEnd(), startFootprint.unzonedRange.getEnd());
12469
-        var dateMutation;
12470
-        var eventDefMutation;
12471
-        if (origRange.getEnd().add(endDelta) > origRange.getStart()) {
12472
-            dateMutation = new EventDefDateMutation_1.default();
12473
-            dateMutation.setEndDelta(endDelta);
12474
-            eventDefMutation = new EventDefMutation_1.default();
12475
-            eventDefMutation.setDateMutation(dateMutation);
12476
-            return eventDefMutation;
12477
+    DateComponent.prototype.prepareHits = function () {
12478
+        // subclasses can implement
12479
+    };
12480
+    DateComponent.prototype.releaseHits = function () {
12481
+        // subclasses can implement
12482
+    };
12483
+    // Given coordinates from the topleft of the document, return data about the date-related area underneath.
12484
+    // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
12485
+    // Must have a `grid` property, a reference to this current grid. TODO: avoid this
12486
+    // The returned object will be processed by getHitFootprint and getHitEl.
12487
+    DateComponent.prototype.queryHit = function (leftOffset, topOffset) {
12488
+        var childrenByUid = this.childrenByUid;
12489
+        var uid;
12490
+        var hit;
12491
+        for (uid in childrenByUid) {
12492
+            hit = childrenByUid[uid].queryHit(leftOffset, topOffset);
12493
+            if (hit) {
12494
+                break;
12495
+            }
12496
         }
12497
-        return false;
12498
+        return hit;
12499
     };
12500
-    return EventResizing;
12501
-}(Interaction_1.default));
12502
-exports.default = EventResizing;
12503
-
12504
-
12505
-/***/ }),
12506
-/* 224 */
12507
-/***/ (function(module, exports, __webpack_require__) {
12508
-
12509
-Object.defineProperty(exports, "__esModule", { value: true });
12510
-var tslib_1 = __webpack_require__(2);
12511
-var util_1 = __webpack_require__(4);
12512
-var EventDefMutation_1 = __webpack_require__(37);
12513
-var EventDefDateMutation_1 = __webpack_require__(50);
12514
-var DragListener_1 = __webpack_require__(54);
12515
-var HitDragListener_1 = __webpack_require__(23);
12516
-var MouseFollower_1 = __webpack_require__(244);
12517
-var Interaction_1 = __webpack_require__(15);
12518
-var EventDragging = /** @class */ (function (_super) {
12519
-    tslib_1.__extends(EventDragging, _super);
12520
-    /*
12521
-    component implements:
12522
-      - bindSegHandlerToEl
12523
-      - publiclyTrigger
12524
-      - diffDates
12525
-      - eventRangesToEventFootprints
12526
-      - isEventInstanceGroupAllowed
12527
-    */
12528
-    function EventDragging(component, eventPointing) {
12529
-        var _this = _super.call(this, component) || this;
12530
-        _this.isDragging = false;
12531
-        _this.eventPointing = eventPointing;
12532
-        return _this;
12533
-    }
12534
-    EventDragging.prototype.end = function () {
12535
-        if (this.dragListener) {
12536
-            this.dragListener.endInteraction();
12537
+    DateComponent.prototype.getSafeHitFootprint = function (hit) {
12538
+        var footprint = this.getHitFootprint(hit);
12539
+        if (!this.dateProfile.activeUnzonedRange.containsRange(footprint.unzonedRange)) {
12540
+            return null;
12541
         }
12542
+        return footprint;
12543
     };
12544
-    EventDragging.prototype.getSelectionDelay = function () {
12545
-        var delay = this.opt('eventLongPressDelay');
12546
-        if (delay == null) {
12547
-            delay = this.opt('longPressDelay'); // fallback
12548
+    DateComponent.prototype.getHitFootprint = function (hit) {
12549
+        // what about being abstract!?
12550
+    };
12551
+    // Given position-level information about a date-related area within the grid,
12552
+    // should return a jQuery element that best represents it. passed to dayClick callback.
12553
+    DateComponent.prototype.getHitEl = function (hit) {
12554
+        // what about being abstract!?
12555
+    };
12556
+    /* Converting eventRange -> eventFootprint
12557
+    ------------------------------------------------------------------------------------------------------------------*/
12558
+    DateComponent.prototype.eventRangesToEventFootprints = function (eventRanges) {
12559
+        var eventFootprints = [];
12560
+        var i;
12561
+        for (i = 0; i < eventRanges.length; i++) {
12562
+            eventFootprints.push.apply(// append
12563
+            eventFootprints, this.eventRangeToEventFootprints(eventRanges[i]));
12564
         }
12565
-        return delay;
12566
+        return eventFootprints;
12567
     };
12568
-    EventDragging.prototype.bindToEl = function (el) {
12569
-        var component = this.component;
12570
-        component.bindSegHandlerToEl(el, 'mousedown', this.handleMousedown.bind(this));
12571
-        component.bindSegHandlerToEl(el, 'touchstart', this.handleTouchStart.bind(this));
12572
+    DateComponent.prototype.eventRangeToEventFootprints = function (eventRange) {
12573
+        return [util_2.eventRangeToEventFootprint(eventRange)];
12574
     };
12575
-    EventDragging.prototype.handleMousedown = function (seg, ev) {
12576
-        if (!this.component.shouldIgnoreMouse() &&
12577
-            this.component.canStartDrag(seg, ev)) {
12578
-            this.buildDragListener(seg).startInteraction(ev, { distance: 5 });
12579
+    /* Converting componentFootprint/eventFootprint -> segs
12580
+    ------------------------------------------------------------------------------------------------------------------*/
12581
+    DateComponent.prototype.eventFootprintsToSegs = function (eventFootprints) {
12582
+        var segs = [];
12583
+        var i;
12584
+        for (i = 0; i < eventFootprints.length; i++) {
12585
+            segs.push.apply(segs, this.eventFootprintToSegs(eventFootprints[i]));
12586
         }
12587
+        return segs;
12588
     };
12589
-    EventDragging.prototype.handleTouchStart = function (seg, ev) {
12590
-        var component = this.component;
12591
-        var settings = {
12592
-            delay: this.view.isEventDefSelected(seg.footprint.eventDef) ? // already selected?
12593
-                0 : this.getSelectionDelay()
12594
+    // Given an event's span (unzoned start/end and other misc data), and the event itself,
12595
+    // slices into segments and attaches event-derived properties to them.
12596
+    // eventSpan - { start, end, isStart, isEnd, otherthings... }
12597
+    DateComponent.prototype.eventFootprintToSegs = function (eventFootprint) {
12598
+        var unzonedRange = eventFootprint.componentFootprint.unzonedRange;
12599
+        var segs;
12600
+        var i;
12601
+        var seg;
12602
+        segs = this.componentFootprintToSegs(eventFootprint.componentFootprint);
12603
+        for (i = 0; i < segs.length; i++) {
12604
+            seg = segs[i];
12605
+            if (!unzonedRange.isStart) {
12606
+                seg.isStart = false;
12607
+            }
12608
+            if (!unzonedRange.isEnd) {
12609
+                seg.isEnd = false;
12610
+            }
12611
+            seg.footprint = eventFootprint;
12612
+            // TODO: rename to seg.eventFootprint
12613
+        }
12614
+        return segs;
12615
+    };
12616
+    DateComponent.prototype.componentFootprintToSegs = function (componentFootprint) {
12617
+        return [];
12618
+    };
12619
+    // Utils
12620
+    // ---------------------------------------------------------------------------------------------------------------
12621
+    DateComponent.prototype.callChildren = function (methodName, args) {
12622
+        this.iterChildren(function (child) {
12623
+            child[methodName].apply(child, args);
12624
+        });
12625
+    };
12626
+    DateComponent.prototype.iterChildren = function (func) {
12627
+        var childrenByUid = this.childrenByUid;
12628
+        var uid;
12629
+        for (uid in childrenByUid) {
12630
+            func(childrenByUid[uid]);
12631
+        }
12632
+    };
12633
+    DateComponent.prototype._getCalendar = function () {
12634
+        var t = this;
12635
+        return t.calendar || t.view.calendar;
12636
+    };
12637
+    DateComponent.prototype._getView = function () {
12638
+        return this.view;
12639
+    };
12640
+    DateComponent.prototype._getDateProfile = function () {
12641
+        return this._getView().get('dateProfile');
12642
+    };
12643
+    // Generates HTML for an anchor to another view into the calendar.
12644
+    // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
12645
+    // `gotoOptions` can either be a moment input, or an object with the form:
12646
+    // { date, type, forceOff }
12647
+    // `type` is a view-type like "day" or "week". default value is "day".
12648
+    // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
12649
+    DateComponent.prototype.buildGotoAnchorHtml = function (gotoOptions, attrs, innerHtml) {
12650
+        var date;
12651
+        var type;
12652
+        var forceOff;
12653
+        var finalOptions;
12654
+        if ($.isPlainObject(gotoOptions)) {
12655
+            date = gotoOptions.date;
12656
+            type = gotoOptions.type;
12657
+            forceOff = gotoOptions.forceOff;
12658
+        }
12659
+        else {
12660
+            date = gotoOptions; // a single moment input
12661
+        }
12662
+        date = moment_ext_1.default(date); // if a string, parse it
12663
+        finalOptions = {
12664
+            date: date.format('YYYY-MM-DD'),
12665
+            type: type || 'day'
12666
         };
12667
-        if (component.canStartDrag(seg, ev)) {
12668
-            this.buildDragListener(seg).startInteraction(ev, settings);
12669
+        if (typeof attrs === 'string') {
12670
+            innerHtml = attrs;
12671
+            attrs = null;
12672
+        }
12673
+        attrs = attrs ? ' ' + util_1.attrsToStr(attrs) : ''; // will have a leading space
12674
+        innerHtml = innerHtml || '';
12675
+        if (!forceOff && this.opt('navLinks')) {
12676
+            return '<a' + attrs +
12677
+                ' data-goto="' + util_1.htmlEscape(JSON.stringify(finalOptions)) + '">' +
12678
+                innerHtml +
12679
+                '</a>';
12680
         }
12681
-        else if (component.canStartSelection(seg, ev)) {
12682
-            this.buildSelectListener(seg).startInteraction(ev, settings);
12683
+        else {
12684
+            return '<span' + attrs + '>' +
12685
+                innerHtml +
12686
+                '</span>';
12687
         }
12688
     };
12689
-    // seg isn't draggable, but let's use a generic DragListener
12690
-    // simply for the delay, so it can be selected.
12691
-    // Has side effect of setting/unsetting `dragListener`
12692
-    EventDragging.prototype.buildSelectListener = function (seg) {
12693
-        var _this = this;
12694
-        var view = this.view;
12695
-        var eventDef = seg.footprint.eventDef;
12696
-        var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
12697
-        if (this.dragListener) {
12698
-            return this.dragListener;
12699
-        }
12700
-        var dragListener = this.dragListener = new DragListener_1.default({
12701
-            dragStart: function (ev) {
12702
-                if (dragListener.isTouch &&
12703
-                    !view.isEventDefSelected(eventDef) &&
12704
-                    eventInstance) {
12705
-                    // if not previously selected, will fire after a delay. then, select the event
12706
-                    view.selectEventInstance(eventInstance);
12707
-                }
12708
-            },
12709
-            interactionEnd: function (ev) {
12710
-                _this.dragListener = null;
12711
-            }
12712
-        });
12713
-        return dragListener;
12714
+    DateComponent.prototype.getAllDayHtml = function () {
12715
+        return this.opt('allDayHtml') || util_1.htmlEscape(this.opt('allDayText'));
12716
     };
12717
-    // Builds a listener that will track user-dragging on an event segment.
12718
-    // Generic enough to work with any type of Grid.
12719
-    // Has side effect of setting/unsetting `dragListener`
12720
-    EventDragging.prototype.buildDragListener = function (seg) {
12721
-        var _this = this;
12722
-        var component = this.component;
12723
-        var view = this.view;
12724
-        var calendar = view.calendar;
12725
-        var eventManager = calendar.eventManager;
12726
-        var el = seg.el;
12727
-        var eventDef = seg.footprint.eventDef;
12728
-        var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
12729
-        var isDragging;
12730
-        var mouseFollower; // A clone of the original element that will move with the mouse
12731
-        var eventDefMutation;
12732
-        if (this.dragListener) {
12733
-            return this.dragListener;
12734
+    // Computes HTML classNames for a single-day element
12735
+    DateComponent.prototype.getDayClasses = function (date, noThemeHighlight) {
12736
+        var view = this._getView();
12737
+        var classes = [];
12738
+        var today;
12739
+        if (!this.dateProfile.activeUnzonedRange.containsDate(date)) {
12740
+            classes.push('fc-disabled-day'); // TODO: jQuery UI theme?
12741
         }
12742
-        // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
12743
-        // of the view.
12744
-        var dragListener = this.dragListener = new HitDragListener_1.default(view, {
12745
-            scroll: this.opt('dragScroll'),
12746
-            subjectEl: el,
12747
-            subjectCenter: true,
12748
-            interactionStart: function (ev) {
12749
-                seg.component = component; // for renderDrag
12750
-                isDragging = false;
12751
-                mouseFollower = new MouseFollower_1.default(seg.el, {
12752
-                    additionalClass: 'fc-dragging',
12753
-                    parentEl: view.el,
12754
-                    opacity: dragListener.isTouch ? null : _this.opt('dragOpacity'),
12755
-                    revertDuration: _this.opt('dragRevertDuration'),
12756
-                    zIndex: 2 // one above the .fc-view
12757
-                });
12758
-                mouseFollower.hide(); // don't show until we know this is a real drag
12759
-                mouseFollower.start(ev);
12760
-            },
12761
-            dragStart: function (ev) {
12762
-                if (dragListener.isTouch &&
12763
-                    !view.isEventDefSelected(eventDef) &&
12764
-                    eventInstance) {
12765
-                    // if not previously selected, will fire after a delay. then, select the event
12766
-                    view.selectEventInstance(eventInstance);
12767
-                }
12768
-                isDragging = true;
12769
-                // ensure a mouseout on the manipulated event has been reported
12770
-                _this.eventPointing.handleMouseout(seg, ev);
12771
-                _this.segDragStart(seg, ev);
12772
-                view.hideEventsWithId(seg.footprint.eventDef.id);
12773
-            },
12774
-            hitOver: function (hit, isOrig, origHit) {
12775
-                var isAllowed = true;
12776
-                var origFootprint;
12777
-                var footprint;
12778
-                var mutatedEventInstanceGroup;
12779
-                // starting hit could be forced (DayGrid.limit)
12780
-                if (seg.hit) {
12781
-                    origHit = seg.hit;
12782
-                }
12783
-                // hit might not belong to this grid, so query origin grid
12784
-                origFootprint = origHit.component.getSafeHitFootprint(origHit);
12785
-                footprint = hit.component.getSafeHitFootprint(hit);
12786
-                if (origFootprint && footprint) {
12787
-                    eventDefMutation = _this.computeEventDropMutation(origFootprint, footprint, eventDef);
12788
-                    if (eventDefMutation) {
12789
-                        mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(eventDef.id, eventDefMutation);
12790
-                        isAllowed = component.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
12791
-                    }
12792
-                    else {
12793
-                        isAllowed = false;
12794
-                    }
12795
-                }
12796
-                else {
12797
-                    isAllowed = false;
12798
-                }
12799
-                if (!isAllowed) {
12800
-                    eventDefMutation = null;
12801
-                    util_1.disableCursor();
12802
-                }
12803
-                // if a valid drop location, have the subclass render a visual indication
12804
-                if (eventDefMutation &&
12805
-                    view.renderDrag(// truthy if rendered something
12806
-                    component.eventRangesToEventFootprints(mutatedEventInstanceGroup.sliceRenderRanges(component.dateProfile.renderUnzonedRange, calendar)), seg, dragListener.isTouch)) {
12807
-                    mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
12808
-                }
12809
-                else {
12810
-                    mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
12811
-                }
12812
-                if (isOrig) {
12813
-                    // needs to have moved hits to be a valid drop
12814
-                    eventDefMutation = null;
12815
+        else {
12816
+            classes.push('fc-' + util_1.dayIDs[date.day()]);
12817
+            if (view.isDateInOtherMonth(date, this.dateProfile)) { // TODO: use DateComponent subclass somehow
12818
+                classes.push('fc-other-month');
12819
+            }
12820
+            today = view.calendar.getNow();
12821
+            if (date.isSame(today, 'day')) {
12822
+                classes.push('fc-today');
12823
+                if (noThemeHighlight !== true) {
12824
+                    classes.push(view.calendar.theme.getClass('today'));
12825
                 }
12826
-            },
12827
-            hitOut: function () {
12828
-                view.unrenderDrag(seg); // unrender whatever was done in renderDrag
12829
-                mouseFollower.show(); // show in case we are moving out of all hits
12830
-                eventDefMutation = null;
12831
-            },
12832
-            hitDone: function () {
12833
-                util_1.enableCursor();
12834
-            },
12835
-            interactionEnd: function (ev) {
12836
-                delete seg.component; // prevent side effects
12837
-                // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
12838
-                mouseFollower.stop(!eventDefMutation, function () {
12839
-                    if (isDragging) {
12840
-                        view.unrenderDrag(seg);
12841
-                        _this.segDragStop(seg, ev);
12842
-                    }
12843
-                    view.showEventsWithId(seg.footprint.eventDef.id);
12844
-                    if (eventDefMutation) {
12845
-                        // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
12846
-                        view.reportEventDrop(eventInstance, eventDefMutation, el, ev);
12847
-                    }
12848
-                });
12849
-                _this.dragListener = null;
12850
             }
12851
-        });
12852
-        return dragListener;
12853
-    };
12854
-    // Called before event segment dragging starts
12855
-    EventDragging.prototype.segDragStart = function (seg, ev) {
12856
-        this.isDragging = true;
12857
-        this.component.publiclyTrigger('eventDragStart', {
12858
-            context: seg.el[0],
12859
-            args: [
12860
-                seg.footprint.getEventLegacy(),
12861
-                ev,
12862
-                {},
12863
-                this.view
12864
-            ]
12865
-        });
12866
-    };
12867
-    // Called after event segment dragging stops
12868
-    EventDragging.prototype.segDragStop = function (seg, ev) {
12869
-        this.isDragging = false;
12870
-        this.component.publiclyTrigger('eventDragStop', {
12871
-            context: seg.el[0],
12872
-            args: [
12873
-                seg.footprint.getEventLegacy(),
12874
-                ev,
12875
-                {},
12876
-                this.view
12877
-            ]
12878
-        });
12879
-    };
12880
-    // DOES NOT consider overlap/constraint
12881
-    EventDragging.prototype.computeEventDropMutation = function (startFootprint, endFootprint, eventDef) {
12882
-        var eventDefMutation = new EventDefMutation_1.default();
12883
-        eventDefMutation.setDateMutation(this.computeEventDateMutation(startFootprint, endFootprint));
12884
-        return eventDefMutation;
12885
-    };
12886
-    EventDragging.prototype.computeEventDateMutation = function (startFootprint, endFootprint) {
12887
-        var date0 = startFootprint.unzonedRange.getStart();
12888
-        var date1 = endFootprint.unzonedRange.getStart();
12889
-        var clearEnd = false;
12890
-        var forceTimed = false;
12891
-        var forceAllDay = false;
12892
-        var dateDelta;
12893
-        var dateMutation;
12894
-        if (startFootprint.isAllDay !== endFootprint.isAllDay) {
12895
-            clearEnd = true;
12896
-            if (endFootprint.isAllDay) {
12897
-                forceAllDay = true;
12898
-                date0.stripTime();
12899
+            else if (date < today) {
12900
+                classes.push('fc-past');
12901
             }
12902
             else {
12903
-                forceTimed = true;
12904
+                classes.push('fc-future');
12905
             }
12906
         }
12907
-        dateDelta = this.component.diffDates(date1, date0);
12908
-        dateMutation = new EventDefDateMutation_1.default();
12909
-        dateMutation.clearEnd = clearEnd;
12910
-        dateMutation.forceTimed = forceTimed;
12911
-        dateMutation.forceAllDay = forceAllDay;
12912
-        dateMutation.setDateDelta(dateDelta);
12913
-        return dateMutation;
12914
+        return classes;
12915
+    };
12916
+    // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
12917
+    // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
12918
+    // The timezones of the dates within `range` will be respected.
12919
+    DateComponent.prototype.formatRange = function (range, isAllDay, formatStr, separator) {
12920
+        var end = range.end;
12921
+        if (isAllDay) {
12922
+            end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
12923
+        }
12924
+        return date_formatting_1.formatRange(range.start, end, formatStr, separator, this.isRTL);
12925
+    };
12926
+    // Compute the number of the give units in the "current" range.
12927
+    // Will return a floating-point number. Won't round.
12928
+    DateComponent.prototype.currentRangeAs = function (unit) {
12929
+        return this._getDateProfile().currentUnzonedRange.as(unit);
12930
+    };
12931
+    // Returns the date range of the full days the given range visually appears to occupy.
12932
+    // Returns a plain object with start/end, NOT an UnzonedRange!
12933
+    DateComponent.prototype.computeDayRange = function (unzonedRange) {
12934
+        var calendar = this._getCalendar();
12935
+        var startDay = calendar.msToUtcMoment(unzonedRange.startMs, true); // the beginning of the day the range starts
12936
+        var end = calendar.msToUtcMoment(unzonedRange.endMs);
12937
+        var endTimeMS = +end.time(); // # of milliseconds into `endDay`
12938
+        var endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
12939
+        // If the end time is actually inclusively part of the next day and is equal to or
12940
+        // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
12941
+        // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
12942
+        if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
12943
+            endDay.add(1, 'days');
12944
+        }
12945
+        // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
12946
+        if (endDay <= startDay) {
12947
+            endDay = startDay.clone().add(1, 'days');
12948
+        }
12949
+        return { start: startDay, end: endDay };
12950
     };
12951
-    return EventDragging;
12952
-}(Interaction_1.default));
12953
-exports.default = EventDragging;
12954
+    // Does the given range visually appear to occupy more than one day?
12955
+    DateComponent.prototype.isMultiDayRange = function (unzonedRange) {
12956
+        var dayRange = this.computeDayRange(unzonedRange);
12957
+        return dayRange.end.diff(dayRange.start, 'days') > 1;
12958
+    };
12959
+    DateComponent.guid = 0; // TODO: better system for this?
12960
+    return DateComponent;
12961
+}(Component_1.default));
12962
+exports.default = DateComponent;
12963
+// legacy
12964
+function convertEventsPayloadToLegacyArray(eventsPayload) {
12965
+    var eventDefId;
12966
+    var eventInstances;
12967
+    var legacyEvents = [];
12968
+    var i;
12969
+    for (eventDefId in eventsPayload) {
12970
+        eventInstances = eventsPayload[eventDefId].eventInstances;
12971
+        for (i = 0; i < eventInstances.length; i++) {
12972
+            legacyEvents.push(eventInstances[i].toLegacy());
12973
+        }
12974
+    }
12975
+    return legacyEvents;
12976
+}
12977
 
12978
 
12979
 /***/ }),
12980
-/* 225 */
12981
+/* 232 */
12982
 /***/ (function(module, exports, __webpack_require__) {
12983
 
12984
 Object.defineProperty(exports, "__esModule", { value: true });
12985
-var tslib_1 = __webpack_require__(2);
12986
+var $ = __webpack_require__(3);
12987
+var moment = __webpack_require__(0);
12988
 var util_1 = __webpack_require__(4);
12989
-var HitDragListener_1 = __webpack_require__(23);
12990
-var ComponentFootprint_1 = __webpack_require__(12);
12991
+var options_1 = __webpack_require__(33);
12992
+var Iterator_1 = __webpack_require__(225);
12993
+var GlobalEmitter_1 = __webpack_require__(23);
12994
+var EmitterMixin_1 = __webpack_require__(13);
12995
+var ListenerMixin_1 = __webpack_require__(7);
12996
+var Toolbar_1 = __webpack_require__(257);
12997
+var OptionsManager_1 = __webpack_require__(258);
12998
+var ViewSpecManager_1 = __webpack_require__(259);
12999
+var Constraints_1 = __webpack_require__(217);
13000
+var locale_1 = __webpack_require__(32);
13001
+var moment_ext_1 = __webpack_require__(11);
13002
 var UnzonedRange_1 = __webpack_require__(5);
13003
-var Interaction_1 = __webpack_require__(15);
13004
-var DateSelecting = /** @class */ (function (_super) {
13005
-    tslib_1.__extends(DateSelecting, _super);
13006
-    /*
13007
-    component must implement:
13008
-      - bindDateHandlerToEl
13009
-      - getSafeHitFootprint
13010
-      - renderHighlight
13011
-      - unrenderHighlight
13012
-    */
13013
-    function DateSelecting(component) {
13014
-        var _this = _super.call(this, component) || this;
13015
-        _this.dragListener = _this.buildDragListener();
13016
-        return _this;
13017
+var ComponentFootprint_1 = __webpack_require__(12);
13018
+var EventDateProfile_1 = __webpack_require__(16);
13019
+var EventManager_1 = __webpack_require__(220);
13020
+var BusinessHourGenerator_1 = __webpack_require__(218);
13021
+var EventSourceParser_1 = __webpack_require__(38);
13022
+var EventDefParser_1 = __webpack_require__(36);
13023
+var SingleEventDef_1 = __webpack_require__(9);
13024
+var EventDefMutation_1 = __webpack_require__(39);
13025
+var EventSource_1 = __webpack_require__(6);
13026
+var ThemeRegistry_1 = __webpack_require__(57);
13027
+var Calendar = /** @class */ (function () {
13028
+    function Calendar(el, overrides) {
13029
+        this.loadingLevel = 0; // number of simultaneous loading tasks
13030
+        this.ignoreUpdateViewSize = 0;
13031
+        this.freezeContentHeightDepth = 0;
13032
+        // declare the current calendar instance relies on GlobalEmitter. needed for garbage collection.
13033
+        // unneeded() is called in destroy.
13034
+        GlobalEmitter_1.default.needed();
13035
+        this.el = el;
13036
+        this.viewsByType = {};
13037
+        this.optionsManager = new OptionsManager_1.default(this, overrides);
13038
+        this.viewSpecManager = new ViewSpecManager_1.default(this.optionsManager, this);
13039
+        this.initMomentInternals(); // needs to happen after options hash initialized
13040
+        this.initCurrentDate();
13041
+        this.initEventManager();
13042
+        this.constraints = new Constraints_1.default(this.eventManager, this);
13043
+        this.constructed();
13044
     }
13045
-    DateSelecting.prototype.end = function () {
13046
-        this.dragListener.endInteraction();
13047
+    Calendar.prototype.constructed = function () {
13048
+        // useful for monkeypatching. used?
13049
     };
13050
-    DateSelecting.prototype.getDelay = function () {
13051
-        var delay = this.opt('selectLongPressDelay');
13052
-        if (delay == null) {
13053
-            delay = this.opt('longPressDelay'); // fallback
13054
+    Calendar.prototype.getView = function () {
13055
+        return this.view;
13056
+    };
13057
+    Calendar.prototype.publiclyTrigger = function (name, triggerInfo) {
13058
+        var optHandler = this.opt(name);
13059
+        var context;
13060
+        var args;
13061
+        if ($.isPlainObject(triggerInfo)) {
13062
+            context = triggerInfo.context;
13063
+            args = triggerInfo.args;
13064
+        }
13065
+        else if ($.isArray(triggerInfo)) {
13066
+            args = triggerInfo;
13067
+        }
13068
+        if (context == null) {
13069
+            context = this.el[0]; // fallback context
13070
+        }
13071
+        if (!args) {
13072
+            args = [];
13073
+        }
13074
+        this.triggerWith(name, context, args); // Emitter's method
13075
+        if (optHandler) {
13076
+            return optHandler.apply(context, args);
13077
         }
13078
-        return delay;
13079
     };
13080
-    DateSelecting.prototype.bindToEl = function (el) {
13081
-        var _this = this;
13082
-        var component = this.component;
13083
-        var dragListener = this.dragListener;
13084
-        component.bindDateHandlerToEl(el, 'mousedown', function (ev) {
13085
-            if (_this.opt('selectable') && !component.shouldIgnoreMouse()) {
13086
-                dragListener.startInteraction(ev, {
13087
-                    distance: _this.opt('selectMinDistance')
13088
-                });
13089
+    Calendar.prototype.hasPublicHandlers = function (name) {
13090
+        return this.hasHandlers(name) ||
13091
+            this.opt(name); // handler specified in options
13092
+    };
13093
+    // Options Public API
13094
+    // -----------------------------------------------------------------------------------------------------------------
13095
+    // public getter/setter
13096
+    Calendar.prototype.option = function (name, value) {
13097
+        var newOptionHash;
13098
+        if (typeof name === 'string') {
13099
+            if (value === undefined) { // getter
13100
+                return this.optionsManager.get(name);
13101
             }
13102
-        });
13103
-        component.bindDateHandlerToEl(el, 'touchstart', function (ev) {
13104
-            if (_this.opt('selectable') && !component.shouldIgnoreTouch()) {
13105
-                dragListener.startInteraction(ev, {
13106
-                    delay: _this.getDelay()
13107
-                });
13108
+            else { // setter for individual option
13109
+                newOptionHash = {};
13110
+                newOptionHash[name] = value;
13111
+                this.optionsManager.add(newOptionHash);
13112
             }
13113
-        });
13114
-        util_1.preventSelection(el);
13115
+        }
13116
+        else if (typeof name === 'object') { // compound setter with object input
13117
+            this.optionsManager.add(name);
13118
+        }
13119
     };
13120
-    // Creates a listener that tracks the user's drag across day elements, for day selecting.
13121
-    DateSelecting.prototype.buildDragListener = function () {
13122
-        var _this = this;
13123
-        var component = this.component;
13124
-        var selectionFootprint; // null if invalid selection
13125
-        var dragListener = new HitDragListener_1.default(component, {
13126
-            scroll: this.opt('dragScroll'),
13127
-            interactionStart: function () {
13128
-                selectionFootprint = null;
13129
-            },
13130
-            dragStart: function (ev) {
13131
-                _this.view.unselect(ev); // since we could be rendering a new selection, we want to clear any old one
13132
-            },
13133
-            hitOver: function (hit, isOrig, origHit) {
13134
-                var origHitFootprint;
13135
-                var hitFootprint;
13136
-                if (origHit) {
13137
-                    origHitFootprint = component.getSafeHitFootprint(origHit);
13138
-                    hitFootprint = component.getSafeHitFootprint(hit);
13139
-                    if (origHitFootprint && hitFootprint) {
13140
-                        selectionFootprint = _this.computeSelection(origHitFootprint, hitFootprint);
13141
-                    }
13142
-                    else {
13143
-                        selectionFootprint = null;
13144
-                    }
13145
-                    if (selectionFootprint) {
13146
-                        component.renderSelectionFootprint(selectionFootprint);
13147
-                    }
13148
-                    else if (selectionFootprint === false) {
13149
-                        util_1.disableCursor();
13150
-                    }
13151
-                }
13152
-            },
13153
-            hitOut: function () {
13154
-                selectionFootprint = null;
13155
-                component.unrenderSelection();
13156
-            },
13157
-            hitDone: function () {
13158
-                util_1.enableCursor();
13159
-            },
13160
-            interactionEnd: function (ev, isCancelled) {
13161
-                if (!isCancelled && selectionFootprint) {
13162
-                    // the selection will already have been rendered. just report it
13163
-                    _this.view.reportSelection(selectionFootprint, ev);
13164
-                }
13165
+    // private getter
13166
+    Calendar.prototype.opt = function (name) {
13167
+        return this.optionsManager.get(name);
13168
+    };
13169
+    // View
13170
+    // -----------------------------------------------------------------------------------------------------------------
13171
+    // Given a view name for a custom view or a standard view, creates a ready-to-go View object
13172
+    Calendar.prototype.instantiateView = function (viewType) {
13173
+        var spec = this.viewSpecManager.getViewSpec(viewType);
13174
+        if (!spec) {
13175
+            throw new Error("View type \"" + viewType + "\" is not valid");
13176
+        }
13177
+        return new spec['class'](this, spec);
13178
+    };
13179
+    // Returns a boolean about whether the view is okay to instantiate at some point
13180
+    Calendar.prototype.isValidViewType = function (viewType) {
13181
+        return Boolean(this.viewSpecManager.getViewSpec(viewType));
13182
+    };
13183
+    Calendar.prototype.changeView = function (viewName, dateOrRange) {
13184
+        if (dateOrRange) {
13185
+            if (dateOrRange.start && dateOrRange.end) { // a range
13186
+                this.optionsManager.recordOverrides({
13187
+                    visibleRange: dateOrRange
13188
+                });
13189
             }
13190
-        });
13191
-        return dragListener;
13192
+            else { // a date
13193
+                this.currentDate = this.moment(dateOrRange).stripZone(); // just like gotoDate
13194
+            }
13195
+        }
13196
+        this.renderView(viewName);
13197
     };
13198
-    // Given the first and last date-spans of a selection, returns another date-span object.
13199
-    // Subclasses can override and provide additional data in the span object. Will be passed to renderSelectionFootprint().
13200
-    // Will return false if the selection is invalid and this should be indicated to the user.
13201
-    // Will return null/undefined if a selection invalid but no error should be reported.
13202
-    DateSelecting.prototype.computeSelection = function (footprint0, footprint1) {
13203
-        var wholeFootprint = this.computeSelectionFootprint(footprint0, footprint1);
13204
-        if (wholeFootprint && !this.isSelectionFootprintAllowed(wholeFootprint)) {
13205
-            return false;
13206
+    // Forces navigation to a view for the given date.
13207
+    // `viewType` can be a specific view name or a generic one like "week" or "day".
13208
+    Calendar.prototype.zoomTo = function (newDate, viewType) {
13209
+        var spec;
13210
+        viewType = viewType || 'day'; // day is default zoom
13211
+        spec = this.viewSpecManager.getViewSpec(viewType) ||
13212
+            this.viewSpecManager.getUnitViewSpec(viewType);
13213
+        this.currentDate = newDate.clone();
13214
+        this.renderView(spec ? spec.type : null);
13215
+    };
13216
+    // Current Date
13217
+    // -----------------------------------------------------------------------------------------------------------------
13218
+    Calendar.prototype.initCurrentDate = function () {
13219
+        var defaultDateInput = this.opt('defaultDate');
13220
+        // compute the initial ambig-timezone date
13221
+        if (defaultDateInput != null) {
13222
+            this.currentDate = this.moment(defaultDateInput).stripZone();
13223
+        }
13224
+        else {
13225
+            this.currentDate = this.getNow(); // getNow already returns unzoned
13226
+        }
13227
+    };
13228
+    Calendar.prototype.prev = function () {
13229
+        var view = this.view;
13230
+        var prevInfo = view.dateProfileGenerator.buildPrev(view.get('dateProfile'));
13231
+        if (prevInfo.isValid) {
13232
+            this.currentDate = prevInfo.date;
13233
+            this.renderView();
13234
+        }
13235
+    };
13236
+    Calendar.prototype.next = function () {
13237
+        var view = this.view;
13238
+        var nextInfo = view.dateProfileGenerator.buildNext(view.get('dateProfile'));
13239
+        if (nextInfo.isValid) {
13240
+            this.currentDate = nextInfo.date;
13241
+            this.renderView();
13242
         }
13243
-        return wholeFootprint;
13244
     };
13245
-    // Given two spans, must return the combination of the two.
13246
-    // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
13247
-    // Assumes both footprints are non-open-ended.
13248
-    DateSelecting.prototype.computeSelectionFootprint = function (footprint0, footprint1) {
13249
-        var ms = [
13250
-            footprint0.unzonedRange.startMs,
13251
-            footprint0.unzonedRange.endMs,
13252
-            footprint1.unzonedRange.startMs,
13253
-            footprint1.unzonedRange.endMs
13254
-        ];
13255
-        ms.sort(util_1.compareNumbers);
13256
-        return new ComponentFootprint_1.default(new UnzonedRange_1.default(ms[0], ms[3]), footprint0.isAllDay);
13257
+    Calendar.prototype.prevYear = function () {
13258
+        this.currentDate.add(-1, 'years');
13259
+        this.renderView();
13260
     };
13261
-    DateSelecting.prototype.isSelectionFootprintAllowed = function (componentFootprint) {
13262
-        return this.component.dateProfile.validUnzonedRange.containsRange(componentFootprint.unzonedRange) &&
13263
-            this.view.calendar.constraints.isSelectionFootprintAllowed(componentFootprint);
13264
+    Calendar.prototype.nextYear = function () {
13265
+        this.currentDate.add(1, 'years');
13266
+        this.renderView();
13267
     };
13268
-    return DateSelecting;
13269
-}(Interaction_1.default));
13270
-exports.default = DateSelecting;
13271
-
13272
-
13273
-/***/ }),
13274
-/* 226 */
13275
-/***/ (function(module, exports, __webpack_require__) {
13276
-
13277
-Object.defineProperty(exports, "__esModule", { value: true });
13278
-var tslib_1 = __webpack_require__(2);
13279
-var moment = __webpack_require__(0);
13280
-var $ = __webpack_require__(3);
13281
-var util_1 = __webpack_require__(4);
13282
-var Scroller_1 = __webpack_require__(39);
13283
-var View_1 = __webpack_require__(41);
13284
-var TimeGrid_1 = __webpack_require__(227);
13285
-var DayGrid_1 = __webpack_require__(61);
13286
-var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
13287
-var agendaTimeGridMethods;
13288
-var agendaDayGridMethods;
13289
-/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
13290
-----------------------------------------------------------------------------------------------------------------------*/
13291
-// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
13292
-// Responsible for managing width/height.
13293
-var AgendaView = /** @class */ (function (_super) {
13294
-    tslib_1.__extends(AgendaView, _super);
13295
-    function AgendaView(calendar, viewSpec) {
13296
-        var _this = _super.call(this, calendar, viewSpec) || this;
13297
-        _this.usesMinMaxTime = true; // indicates that minTime/maxTime affects rendering
13298
-        _this.timeGrid = _this.instantiateTimeGrid();
13299
-        _this.addChild(_this.timeGrid);
13300
-        if (_this.opt('allDaySlot')) {
13301
-            _this.dayGrid = _this.instantiateDayGrid(); // the all-day subcomponent of this view
13302
-            _this.addChild(_this.dayGrid);
13303
-        }
13304
-        _this.scroller = new Scroller_1.default({
13305
-            overflowX: 'hidden',
13306
-            overflowY: 'auto'
13307
-        });
13308
-        return _this;
13309
-    }
13310
-    // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass
13311
-    AgendaView.prototype.instantiateTimeGrid = function () {
13312
-        var timeGrid = new this.timeGridClass(this);
13313
-        util_1.copyOwnProps(agendaTimeGridMethods, timeGrid);
13314
-        return timeGrid;
13315
+    Calendar.prototype.today = function () {
13316
+        this.currentDate = this.getNow(); // should deny like prev/next?
13317
+        this.renderView();
13318
     };
13319
-    // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass
13320
-    AgendaView.prototype.instantiateDayGrid = function () {
13321
-        var dayGrid = new this.dayGridClass(this);
13322
-        util_1.copyOwnProps(agendaDayGridMethods, dayGrid);
13323
-        return dayGrid;
13324
+    Calendar.prototype.gotoDate = function (zonedDateInput) {
13325
+        this.currentDate = this.moment(zonedDateInput).stripZone();
13326
+        this.renderView();
13327
     };
13328
-    /* Rendering
13329
-    ------------------------------------------------------------------------------------------------------------------*/
13330
-    AgendaView.prototype.renderSkeleton = function () {
13331
-        var timeGridWrapEl;
13332
-        var timeGridEl;
13333
-        this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml());
13334
-        this.scroller.render();
13335
-        timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container');
13336
-        timeGridEl = $('<div class="fc-time-grid" />').appendTo(timeGridWrapEl);
13337
-        this.el.find('.fc-body > tr > td').append(timeGridWrapEl);
13338
-        this.timeGrid.headContainerEl = this.el.find('.fc-head-container');
13339
-        this.timeGrid.setElement(timeGridEl);
13340
-        if (this.dayGrid) {
13341
-            this.dayGrid.setElement(this.el.find('.fc-day-grid'));
13342
-            // have the day-grid extend it's coordinate area over the <hr> dividing the two grids
13343
-            this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
13344
-        }
13345
+    Calendar.prototype.incrementDate = function (delta) {
13346
+        this.currentDate.add(moment.duration(delta));
13347
+        this.renderView();
13348
     };
13349
-    AgendaView.prototype.unrenderSkeleton = function () {
13350
-        this.timeGrid.removeElement();
13351
-        if (this.dayGrid) {
13352
-            this.dayGrid.removeElement();
13353
-        }
13354
-        this.scroller.destroy();
13355
+    // for external API
13356
+    Calendar.prototype.getDate = function () {
13357
+        return this.applyTimezone(this.currentDate); // infuse the calendar's timezone
13358
     };
13359
-    // Builds the HTML skeleton for the view.
13360
-    // The day-grid and time-grid components will render inside containers defined by this HTML.
13361
-    AgendaView.prototype.renderSkeletonHtml = function () {
13362
-        var theme = this.calendar.theme;
13363
-        return '' +
13364
-            '<table class="' + theme.getClass('tableGrid') + '">' +
13365
-            (this.opt('columnHeader') ?
13366
-                '<thead class="fc-head">' +
13367
-                    '<tr>' +
13368
-                    '<td class="fc-head-container ' + theme.getClass('widgetHeader') + '">&nbsp;</td>' +
13369
-                    '</tr>' +
13370
-                    '</thead>' :
13371
-                '') +
13372
-            '<tbody class="fc-body">' +
13373
-            '<tr>' +
13374
-            '<td class="' + theme.getClass('widgetContent') + '">' +
13375
-            (this.dayGrid ?
13376
-                '<div class="fc-day-grid"/>' +
13377
-                    '<hr class="fc-divider ' + theme.getClass('widgetHeader') + '"/>' :
13378
-                '') +
13379
-            '</td>' +
13380
-            '</tr>' +
13381
-            '</tbody>' +
13382
-            '</table>';
13383
+    // Loading Triggering
13384
+    // -----------------------------------------------------------------------------------------------------------------
13385
+    // Should be called when any type of async data fetching begins
13386
+    Calendar.prototype.pushLoading = function () {
13387
+        if (!(this.loadingLevel++)) {
13388
+            this.publiclyTrigger('loading', [true, this.view]);
13389
+        }
13390
     };
13391
-    // Generates an HTML attribute string for setting the width of the axis, if it is known
13392
-    AgendaView.prototype.axisStyleAttr = function () {
13393
-        if (this.axisWidth != null) {
13394
-            return 'style="width:' + this.axisWidth + 'px"';
13395
+    // Should be called when any type of async data fetching completes
13396
+    Calendar.prototype.popLoading = function () {
13397
+        if (!(--this.loadingLevel)) {
13398
+            this.publiclyTrigger('loading', [false, this.view]);
13399
         }
13400
-        return '';
13401
     };
13402
-    /* Now Indicator
13403
-    ------------------------------------------------------------------------------------------------------------------*/
13404
-    AgendaView.prototype.getNowIndicatorUnit = function () {
13405
-        return this.timeGrid.getNowIndicatorUnit();
13406
+    // High-level Rendering
13407
+    // -----------------------------------------------------------------------------------
13408
+    Calendar.prototype.render = function () {
13409
+        if (!this.contentEl) {
13410
+            this.initialRender();
13411
+        }
13412
+        else if (this.elementVisible()) {
13413
+            // mainly for the public API
13414
+            this.calcSize();
13415
+            this.updateViewSize();
13416
+        }
13417
     };
13418
-    /* Dimensions
13419
-    ------------------------------------------------------------------------------------------------------------------*/
13420
-    // Adjusts the vertical dimensions of the view to the specified values
13421
-    AgendaView.prototype.updateSize = function (totalHeight, isAuto, isResize) {
13422
-        var eventLimit;
13423
-        var scrollerHeight;
13424
-        var scrollbarWidths;
13425
-        _super.prototype.updateSize.call(this, totalHeight, isAuto, isResize);
13426
-        // make all axis cells line up, and record the width so newly created axis cells will have it
13427
-        this.axisWidth = util_1.matchCellWidths(this.el.find('.fc-axis'));
13428
-        // hack to give the view some height prior to timeGrid's columns being rendered
13429
-        // TODO: separate setting height from scroller VS timeGrid.
13430
-        if (!this.timeGrid.colEls) {
13431
-            if (!isAuto) {
13432
-                scrollerHeight = this.computeScrollerHeight(totalHeight);
13433
-                this.scroller.setHeight(scrollerHeight);
13434
+    Calendar.prototype.initialRender = function () {
13435
+        var _this = this;
13436
+        var el = this.el;
13437
+        el.addClass('fc');
13438
+        // event delegation for nav links
13439
+        el.on('click.fc', 'a[data-goto]', function (ev) {
13440
+            var anchorEl = $(ev.currentTarget);
13441
+            var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON
13442
+            var date = _this.moment(gotoOptions.date);
13443
+            var viewType = gotoOptions.type;
13444
+            // property like "navLinkDayClick". might be a string or a function
13445
+            var customAction = _this.view.opt('navLink' + util_1.capitaliseFirstLetter(viewType) + 'Click');
13446
+            if (typeof customAction === 'function') {
13447
+                customAction(date, ev);
13448
             }
13449
-            return;
13450
+            else {
13451
+                if (typeof customAction === 'string') {
13452
+                    viewType = customAction;
13453
+                }
13454
+                _this.zoomTo(date, viewType);
13455
+            }
13456
+        });
13457
+        // called immediately, and upon option change
13458
+        this.optionsManager.watch('settingTheme', ['?theme', '?themeSystem'], function (opts) {
13459
+            var themeClass = ThemeRegistry_1.getThemeSystemClass(opts.themeSystem || opts.theme);
13460
+            var theme = new themeClass(_this.optionsManager);
13461
+            var widgetClass = theme.getClass('widget');
13462
+            _this.theme = theme;
13463
+            if (widgetClass) {
13464
+                el.addClass(widgetClass);
13465
+            }
13466
+        }, function () {
13467
+            var widgetClass = _this.theme.getClass('widget');
13468
+            _this.theme = null;
13469
+            if (widgetClass) {
13470
+                el.removeClass(widgetClass);
13471
+            }
13472
+        });
13473
+        this.optionsManager.watch('settingBusinessHourGenerator', ['?businessHours'], function (deps) {
13474
+            _this.businessHourGenerator = new BusinessHourGenerator_1.default(deps.businessHours, _this);
13475
+            if (_this.view) {
13476
+                _this.view.set('businessHourGenerator', _this.businessHourGenerator);
13477
+            }
13478
+        }, function () {
13479
+            _this.businessHourGenerator = null;
13480
+        });
13481
+        // called immediately, and upon option change.
13482
+        // HACK: locale often affects isRTL, so we explicitly listen to that too.
13483
+        this.optionsManager.watch('applyingDirClasses', ['?isRTL', '?locale'], function (opts) {
13484
+            el.toggleClass('fc-ltr', !opts.isRTL);
13485
+            el.toggleClass('fc-rtl', opts.isRTL);
13486
+        });
13487
+        this.contentEl = $("<div class='fc-view-container'>").prependTo(el);
13488
+        this.initToolbars();
13489
+        this.renderHeader();
13490
+        this.renderFooter();
13491
+        this.renderView(this.opt('defaultView'));
13492
+        if (this.opt('handleWindowResize')) {
13493
+            $(window).resize(this.windowResizeProxy = util_1.debounce(// prevents rapid calls
13494
+            this.windowResize.bind(this), this.opt('windowResizeDelay')));
13495
         }
13496
-        // set of fake row elements that must compensate when scroller has scrollbars
13497
-        var noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)');
13498
-        // reset all dimensions back to the original state
13499
-        this.timeGrid.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
13500
-        this.scroller.clear(); // sets height to 'auto' and clears overflow
13501
-        util_1.uncompensateScroll(noScrollRowEls);
13502
-        // limit number of events in the all-day area
13503
-        if (this.dayGrid) {
13504
-            this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
13505
-            eventLimit = this.opt('eventLimit');
13506
-            if (eventLimit && typeof eventLimit !== 'number') {
13507
-                eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number
13508
+    };
13509
+    Calendar.prototype.destroy = function () {
13510
+        if (this.view) {
13511
+            this.clearView();
13512
+        }
13513
+        this.toolbarsManager.proxyCall('removeElement');
13514
+        this.contentEl.remove();
13515
+        this.el.removeClass('fc fc-ltr fc-rtl');
13516
+        // removes theme-related root className
13517
+        this.optionsManager.unwatch('settingTheme');
13518
+        this.optionsManager.unwatch('settingBusinessHourGenerator');
13519
+        this.el.off('.fc'); // unbind nav link handlers
13520
+        if (this.windowResizeProxy) {
13521
+            $(window).unbind('resize', this.windowResizeProxy);
13522
+            this.windowResizeProxy = null;
13523
+        }
13524
+        GlobalEmitter_1.default.unneeded();
13525
+    };
13526
+    Calendar.prototype.elementVisible = function () {
13527
+        return this.el.is(':visible');
13528
+    };
13529
+    // Render Queue
13530
+    // -----------------------------------------------------------------------------------------------------------------
13531
+    Calendar.prototype.bindViewHandlers = function (view) {
13532
+        var _this = this;
13533
+        view.watch('titleForCalendar', ['title'], function (deps) {
13534
+            if (view === _this.view) { // hack
13535
+                _this.setToolbarsTitle(deps.title);
13536
             }
13537
-            if (eventLimit) {
13538
-                this.dayGrid.limitRows(eventLimit);
13539
+        });
13540
+        view.watch('dateProfileForCalendar', ['dateProfile'], function (deps) {
13541
+            if (view === _this.view) { // hack
13542
+                _this.currentDate = deps.dateProfile.date; // might have been constrained by view dates
13543
+                _this.updateToolbarButtons(deps.dateProfile);
13544
             }
13545
+        });
13546
+    };
13547
+    Calendar.prototype.unbindViewHandlers = function (view) {
13548
+        view.unwatch('titleForCalendar');
13549
+        view.unwatch('dateProfileForCalendar');
13550
+    };
13551
+    // View Rendering
13552
+    // -----------------------------------------------------------------------------------
13553
+    // Renders a view because of a date change, view-type change, or for the first time.
13554
+    // If not given a viewType, keep the current view but render different dates.
13555
+    // Accepts an optional scroll state to restore to.
13556
+    Calendar.prototype.renderView = function (viewType) {
13557
+        var oldView = this.view;
13558
+        var newView;
13559
+        this.freezeContentHeight();
13560
+        if (oldView && viewType && oldView.type !== viewType) {
13561
+            this.clearView();
13562
         }
13563
-        if (!isAuto) {
13564
-            scrollerHeight = this.computeScrollerHeight(totalHeight);
13565
-            this.scroller.setHeight(scrollerHeight);
13566
-            scrollbarWidths = this.scroller.getScrollbarWidths();
13567
-            if (scrollbarWidths.left || scrollbarWidths.right) {
13568
-                // make the all-day and header rows lines up
13569
-                util_1.compensateScroll(noScrollRowEls, scrollbarWidths);
13570
-                // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
13571
-                // and reapply the desired height to the scroller.
13572
-                scrollerHeight = this.computeScrollerHeight(totalHeight);
13573
-                this.scroller.setHeight(scrollerHeight);
13574
+        // if viewType changed, or the view was never created, create a fresh view
13575
+        if (!this.view && viewType) {
13576
+            newView = this.view =
13577
+                this.viewsByType[viewType] ||
13578
+                    (this.viewsByType[viewType] = this.instantiateView(viewType));
13579
+            this.bindViewHandlers(newView);
13580
+            newView.startBatchRender(); // so that setElement+setDate rendering are joined
13581
+            newView.setElement($("<div class='fc-view fc-" + viewType + "-view'>").appendTo(this.contentEl));
13582
+            this.toolbarsManager.proxyCall('activateButton', viewType);
13583
+        }
13584
+        if (this.view) {
13585
+            // prevent unnecessary change firing
13586
+            if (this.view.get('businessHourGenerator') !== this.businessHourGenerator) {
13587
+                this.view.set('businessHourGenerator', this.businessHourGenerator);
13588
             }
13589
-            // guarantees the same scrollbar widths
13590
-            this.scroller.lockOverflow(scrollbarWidths);
13591
-            // if there's any space below the slats, show the horizontal rule.
13592
-            // this won't cause any new overflow, because lockOverflow already called.
13593
-            if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) {
13594
-                this.timeGrid.bottomRuleEl.show();
13595
+            this.view.setDate(this.currentDate);
13596
+            if (newView) {
13597
+                newView.stopBatchRender();
13598
             }
13599
         }
13600
+        this.thawContentHeight();
13601
     };
13602
-    // given a desired total height of the view, returns what the height of the scroller should be
13603
-    AgendaView.prototype.computeScrollerHeight = function (totalHeight) {
13604
-        return totalHeight -
13605
-            util_1.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
13606
+    // Unrenders the current view and reflects this change in the Header.
13607
+    // Unregsiters the `view`, but does not remove from viewByType hash.
13608
+    Calendar.prototype.clearView = function () {
13609
+        var currentView = this.view;
13610
+        this.toolbarsManager.proxyCall('deactivateButton', currentView.type);
13611
+        this.unbindViewHandlers(currentView);
13612
+        currentView.removeElement();
13613
+        currentView.unsetDate(); // so bindViewHandlers doesn't fire with old values next time
13614
+        this.view = null;
13615
     };
13616
-    /* Scroll
13617
-    ------------------------------------------------------------------------------------------------------------------*/
13618
-    // Computes the initial pre-configured scroll state prior to allowing the user to change it
13619
-    AgendaView.prototype.computeInitialDateScroll = function () {
13620
-        var scrollTime = moment.duration(this.opt('scrollTime'));
13621
-        var top = this.timeGrid.computeTimeTop(scrollTime);
13622
-        // zoom can give weird floating-point values. rather scroll a little bit further
13623
-        top = Math.ceil(top);
13624
-        if (top) {
13625
-            top++; // to overcome top border that slots beyond the first have. looks better
13626
+    // Destroys the view, including the view object. Then, re-instantiates it and renders it.
13627
+    // Maintains the same scroll state.
13628
+    // TODO: maintain any other user-manipulated state.
13629
+    Calendar.prototype.reinitView = function () {
13630
+        var oldView = this.view;
13631
+        var scroll = oldView.queryScroll(); // wouldn't be so complicated if Calendar owned the scroll
13632
+        this.freezeContentHeight();
13633
+        this.clearView();
13634
+        this.calcSize();
13635
+        this.renderView(oldView.type); // needs the type to freshly render
13636
+        this.view.applyScroll(scroll);
13637
+        this.thawContentHeight();
13638
+    };
13639
+    // Resizing
13640
+    // -----------------------------------------------------------------------------------
13641
+    Calendar.prototype.getSuggestedViewHeight = function () {
13642
+        if (this.suggestedViewHeight == null) {
13643
+            this.calcSize();
13644
         }
13645
-        return { top: top };
13646
+        return this.suggestedViewHeight;
13647
     };
13648
-    AgendaView.prototype.queryDateScroll = function () {
13649
-        return { top: this.scroller.getScrollTop() };
13650
+    Calendar.prototype.isHeightAuto = function () {
13651
+        return this.opt('contentHeight') === 'auto' || this.opt('height') === 'auto';
13652
     };
13653
-    AgendaView.prototype.applyDateScroll = function (scroll) {
13654
-        if (scroll.top !== undefined) {
13655
-            this.scroller.setScrollTop(scroll.top);
13656
+    Calendar.prototype.updateViewSize = function (isResize) {
13657
+        if (isResize === void 0) { isResize = false; }
13658
+        var view = this.view;
13659
+        var scroll;
13660
+        if (!this.ignoreUpdateViewSize && view) {
13661
+            if (isResize) {
13662
+                this.calcSize();
13663
+                scroll = view.queryScroll();
13664
+            }
13665
+            this.ignoreUpdateViewSize++;
13666
+            view.updateSize(this.getSuggestedViewHeight(), this.isHeightAuto(), isResize);
13667
+            this.ignoreUpdateViewSize--;
13668
+            if (isResize) {
13669
+                view.applyScroll(scroll);
13670
+            }
13671
+            return true; // signal success
13672
         }
13673
     };
13674
-    /* Hit Areas
13675
-    ------------------------------------------------------------------------------------------------------------------*/
13676
-    // forward all hit-related method calls to the grids (dayGrid might not be defined)
13677
-    AgendaView.prototype.getHitFootprint = function (hit) {
13678
-        // TODO: hit.component is set as a hack to identify where the hit came from
13679
-        return hit.component.getHitFootprint(hit);
13680
+    Calendar.prototype.calcSize = function () {
13681
+        if (this.elementVisible()) {
13682
+            this._calcSize();
13683
+        }
13684
     };
13685
-    AgendaView.prototype.getHitEl = function (hit) {
13686
-        // TODO: hit.component is set as a hack to identify where the hit came from
13687
-        return hit.component.getHitEl(hit);
13688
+    Calendar.prototype._calcSize = function () {
13689
+        var contentHeightInput = this.opt('contentHeight');
13690
+        var heightInput = this.opt('height');
13691
+        if (typeof contentHeightInput === 'number') { // exists and not 'auto'
13692
+            this.suggestedViewHeight = contentHeightInput;
13693
+        }
13694
+        else if (typeof contentHeightInput === 'function') { // exists and is a function
13695
+            this.suggestedViewHeight = contentHeightInput();
13696
+        }
13697
+        else if (typeof heightInput === 'number') { // exists and not 'auto'
13698
+            this.suggestedViewHeight = heightInput - this.queryToolbarsHeight();
13699
+        }
13700
+        else if (typeof heightInput === 'function') { // exists and is a function
13701
+            this.suggestedViewHeight = heightInput() - this.queryToolbarsHeight();
13702
+        }
13703
+        else if (heightInput === 'parent') { // set to height of parent element
13704
+            this.suggestedViewHeight = this.el.parent().height() - this.queryToolbarsHeight();
13705
+        }
13706
+        else {
13707
+            this.suggestedViewHeight = Math.round(this.contentEl.width() /
13708
+                Math.max(this.opt('aspectRatio'), .5));
13709
+        }
13710
     };
13711
-    /* Event Rendering
13712
-    ------------------------------------------------------------------------------------------------------------------*/
13713
-    AgendaView.prototype.executeEventRender = function (eventsPayload) {
13714
-        var dayEventsPayload = {};
13715
-        var timedEventsPayload = {};
13716
-        var id;
13717
-        var eventInstanceGroup;
13718
-        // separate the events into all-day and timed
13719
-        for (id in eventsPayload) {
13720
-            eventInstanceGroup = eventsPayload[id];
13721
-            if (eventInstanceGroup.getEventDef().isAllDay()) {
13722
-                dayEventsPayload[id] = eventInstanceGroup;
13723
-            }
13724
-            else {
13725
-                timedEventsPayload[id] = eventInstanceGroup;
13726
+    Calendar.prototype.windowResize = function (ev) {
13727
+        if (
13728
+        // the purpose: so we don't process jqui "resize" events that have bubbled up
13729
+        // cast to any because .target, which is Element, can't be compared to window for some reason.
13730
+        ev.target === window &&
13731
+            this.view &&
13732
+            this.view.isDatesRendered) {
13733
+            if (this.updateViewSize(true)) { // isResize=true, returns true on success
13734
+                this.publiclyTrigger('windowResize', [this.view]);
13735
             }
13736
         }
13737
-        this.timeGrid.executeEventRender(timedEventsPayload);
13738
-        if (this.dayGrid) {
13739
-            this.dayGrid.executeEventRender(dayEventsPayload);
13740
+    };
13741
+    /* Height "Freezing"
13742
+    -----------------------------------------------------------------------------*/
13743
+    Calendar.prototype.freezeContentHeight = function () {
13744
+        if (!(this.freezeContentHeightDepth++)) {
13745
+            this.forceFreezeContentHeight();
13746
         }
13747
     };
13748
-    /* Dragging/Resizing Routing
13749
-    ------------------------------------------------------------------------------------------------------------------*/
13750
-    // A returned value of `true` signals that a mock "helper" event has been rendered.
13751
-    AgendaView.prototype.renderDrag = function (eventFootprints, seg, isTouch) {
13752
-        var groups = groupEventFootprintsByAllDay(eventFootprints);
13753
-        var renderedHelper = false;
13754
-        renderedHelper = this.timeGrid.renderDrag(groups.timed, seg, isTouch);
13755
-        if (this.dayGrid) {
13756
-            renderedHelper = this.dayGrid.renderDrag(groups.allDay, seg, isTouch) || renderedHelper;
13757
+    Calendar.prototype.forceFreezeContentHeight = function () {
13758
+        this.contentEl.css({
13759
+            width: '100%',
13760
+            height: this.contentEl.height(),
13761
+            overflow: 'hidden'
13762
+        });
13763
+    };
13764
+    Calendar.prototype.thawContentHeight = function () {
13765
+        this.freezeContentHeightDepth--;
13766
+        // always bring back to natural height
13767
+        this.contentEl.css({
13768
+            width: '',
13769
+            height: '',
13770
+            overflow: ''
13771
+        });
13772
+        // but if there are future thaws, re-freeze
13773
+        if (this.freezeContentHeightDepth) {
13774
+            this.forceFreezeContentHeight();
13775
         }
13776
-        return renderedHelper;
13777
     };
13778
-    AgendaView.prototype.renderEventResize = function (eventFootprints, seg, isTouch) {
13779
-        var groups = groupEventFootprintsByAllDay(eventFootprints);
13780
-        this.timeGrid.renderEventResize(groups.timed, seg, isTouch);
13781
-        if (this.dayGrid) {
13782
-            this.dayGrid.renderEventResize(groups.allDay, seg, isTouch);
13783
+    // Toolbar
13784
+    // -----------------------------------------------------------------------------------------------------------------
13785
+    Calendar.prototype.initToolbars = function () {
13786
+        this.header = new Toolbar_1.default(this, this.computeHeaderOptions());
13787
+        this.footer = new Toolbar_1.default(this, this.computeFooterOptions());
13788
+        this.toolbarsManager = new Iterator_1.default([this.header, this.footer]);
13789
+    };
13790
+    Calendar.prototype.computeHeaderOptions = function () {
13791
+        return {
13792
+            extraClasses: 'fc-header-toolbar',
13793
+            layout: this.opt('header')
13794
+        };
13795
+    };
13796
+    Calendar.prototype.computeFooterOptions = function () {
13797
+        return {
13798
+            extraClasses: 'fc-footer-toolbar',
13799
+            layout: this.opt('footer')
13800
+        };
13801
+    };
13802
+    // can be called repeatedly and Header will rerender
13803
+    Calendar.prototype.renderHeader = function () {
13804
+        var header = this.header;
13805
+        header.setToolbarOptions(this.computeHeaderOptions());
13806
+        header.render();
13807
+        if (header.el) {
13808
+            this.el.prepend(header.el);
13809
         }
13810
     };
13811
-    /* Selection
13812
-    ------------------------------------------------------------------------------------------------------------------*/
13813
-    // Renders a visual indication of a selection
13814
-    AgendaView.prototype.renderSelectionFootprint = function (componentFootprint) {
13815
-        if (!componentFootprint.isAllDay) {
13816
-            this.timeGrid.renderSelectionFootprint(componentFootprint);
13817
-        }
13818
-        else if (this.dayGrid) {
13819
-            this.dayGrid.renderSelectionFootprint(componentFootprint);
13820
+    // can be called repeatedly and Footer will rerender
13821
+    Calendar.prototype.renderFooter = function () {
13822
+        var footer = this.footer;
13823
+        footer.setToolbarOptions(this.computeFooterOptions());
13824
+        footer.render();
13825
+        if (footer.el) {
13826
+            this.el.append(footer.el);
13827
         }
13828
     };
13829
-    return AgendaView;
13830
-}(View_1.default));
13831
-exports.default = AgendaView;
13832
-AgendaView.prototype.timeGridClass = TimeGrid_1.default;
13833
-AgendaView.prototype.dayGridClass = DayGrid_1.default;
13834
-// Will customize the rendering behavior of the AgendaView's timeGrid
13835
-agendaTimeGridMethods = {
13836
-    // Generates the HTML that will go before the day-of week header cells
13837
-    renderHeadIntroHtml: function () {
13838
+    Calendar.prototype.setToolbarsTitle = function (title) {
13839
+        this.toolbarsManager.proxyCall('updateTitle', title);
13840
+    };
13841
+    Calendar.prototype.updateToolbarButtons = function (dateProfile) {
13842
+        var now = this.getNow();
13843
         var view = this.view;
13844
-        var calendar = view.calendar;
13845
-        var weekStart = calendar.msToUtcMoment(this.dateProfile.renderUnzonedRange.startMs, true);
13846
-        var weekText;
13847
-        if (this.opt('weekNumbers')) {
13848
-            weekText = weekStart.format(this.opt('smallWeekFormat'));
13849
-            return '' +
13850
-                '<th class="fc-axis fc-week-number ' + calendar.theme.getClass('widgetHeader') + '" ' + view.axisStyleAttr() + '>' +
13851
-                view.buildGotoAnchorHtml(// aside from link, important for matchCellWidths
13852
-                { date: weekStart, type: 'week', forceOff: this.colCnt > 1 }, util_1.htmlEscape(weekText) // inner HTML
13853
-                ) +
13854
-                '</th>';
13855
+        var todayInfo = view.dateProfileGenerator.build(now);
13856
+        var prevInfo = view.dateProfileGenerator.buildPrev(view.get('dateProfile'));
13857
+        var nextInfo = view.dateProfileGenerator.buildNext(view.get('dateProfile'));
13858
+        this.toolbarsManager.proxyCall((todayInfo.isValid && !dateProfile.currentUnzonedRange.containsDate(now)) ?
13859
+            'enableButton' :
13860
+            'disableButton', 'today');
13861
+        this.toolbarsManager.proxyCall(prevInfo.isValid ?
13862
+            'enableButton' :
13863
+            'disableButton', 'prev');
13864
+        this.toolbarsManager.proxyCall(nextInfo.isValid ?
13865
+            'enableButton' :
13866
+            'disableButton', 'next');
13867
+    };
13868
+    Calendar.prototype.queryToolbarsHeight = function () {
13869
+        return this.toolbarsManager.items.reduce(function (accumulator, toolbar) {
13870
+            var toolbarHeight = toolbar.el ? toolbar.el.outerHeight(true) : 0; // includes margin
13871
+            return accumulator + toolbarHeight;
13872
+        }, 0);
13873
+    };
13874
+    // Selection
13875
+    // -----------------------------------------------------------------------------------------------------------------
13876
+    // this public method receives start/end dates in any format, with any timezone
13877
+    Calendar.prototype.select = function (zonedStartInput, zonedEndInput) {
13878
+        this.view.select(this.buildSelectFootprint.apply(this, arguments));
13879
+    };
13880
+    Calendar.prototype.unselect = function () {
13881
+        if (this.view) {
13882
+            this.view.unselect();
13883
         }
13884
-        else {
13885
-            return '<th class="fc-axis ' + calendar.theme.getClass('widgetHeader') + '" ' + view.axisStyleAttr() + '></th>';
13886
+    };
13887
+    // Given arguments to the select method in the API, returns a span (unzoned start/end and other info)
13888
+    Calendar.prototype.buildSelectFootprint = function (zonedStartInput, zonedEndInput) {
13889
+        var start = this.moment(zonedStartInput).stripZone();
13890
+        var end;
13891
+        if (zonedEndInput) {
13892
+            end = this.moment(zonedEndInput).stripZone();
13893
         }
13894
-    },
13895
-    // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
13896
-    renderBgIntroHtml: function () {
13897
-        var view = this.view;
13898
-        return '<td class="fc-axis ' + view.calendar.theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '></td>';
13899
-    },
13900
-    // Generates the HTML that goes before all other types of cells.
13901
-    // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
13902
-    renderIntroHtml: function () {
13903
-        var view = this.view;
13904
-        return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
13905
-    }
13906
-};
13907
-// Will customize the rendering behavior of the AgendaView's dayGrid
13908
-agendaDayGridMethods = {
13909
-    // Generates the HTML that goes before the all-day cells
13910
-    renderBgIntroHtml: function () {
13911
-        var view = this.view;
13912
-        return '' +
13913
-            '<td class="fc-axis ' + view.calendar.theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '>' +
13914
-            '<span>' + // needed for matchCellWidths
13915
-            view.getAllDayHtml() +
13916
-            '</span>' +
13917
-            '</td>';
13918
-    },
13919
-    // Generates the HTML that goes before all other types of cells.
13920
-    // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
13921
-    renderIntroHtml: function () {
13922
-        var view = this.view;
13923
-        return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
13924
-    }
13925
-};
13926
-function groupEventFootprintsByAllDay(eventFootprints) {
13927
-    var allDay = [];
13928
-    var timed = [];
13929
-    var i;
13930
-    for (i = 0; i < eventFootprints.length; i++) {
13931
-        if (eventFootprints[i].componentFootprint.isAllDay) {
13932
-            allDay.push(eventFootprints[i]);
13933
+        else if (start.hasTime()) {
13934
+            end = start.clone().add(this.defaultTimedEventDuration);
13935
         }
13936
         else {
13937
-            timed.push(eventFootprints[i]);
13938
+            end = start.clone().add(this.defaultAllDayEventDuration);
13939
         }
13940
-    }
13941
-    return { allDay: allDay, timed: timed };
13942
-}
13943
-
13944
-
13945
-/***/ }),
13946
-/* 227 */
13947
-/***/ (function(module, exports, __webpack_require__) {
13948
-
13949
-Object.defineProperty(exports, "__esModule", { value: true });
13950
-var tslib_1 = __webpack_require__(2);
13951
-var $ = __webpack_require__(3);
13952
-var moment = __webpack_require__(0);
13953
-var util_1 = __webpack_require__(4);
13954
-var InteractiveDateComponent_1 = __webpack_require__(40);
13955
-var BusinessHourRenderer_1 = __webpack_require__(56);
13956
-var StandardInteractionsMixin_1 = __webpack_require__(60);
13957
-var DayTableMixin_1 = __webpack_require__(55);
13958
-var CoordCache_1 = __webpack_require__(53);
13959
-var UnzonedRange_1 = __webpack_require__(5);
13960
-var ComponentFootprint_1 = __webpack_require__(12);
13961
-var TimeGridEventRenderer_1 = __webpack_require__(246);
13962
-var TimeGridHelperRenderer_1 = __webpack_require__(247);
13963
-var TimeGridFillRenderer_1 = __webpack_require__(248);
13964
-/* A component that renders one or more columns of vertical time slots
13965
-----------------------------------------------------------------------------------------------------------------------*/
13966
-// We mixin DayTable, even though there is only a single row of days
13967
-// potential nice values for the slot-duration and interval-duration
13968
-// from largest to smallest
13969
-var AGENDA_STOCK_SUB_DURATIONS = [
13970
-    { hours: 1 },
13971
-    { minutes: 30 },
13972
-    { minutes: 15 },
13973
-    { seconds: 30 },
13974
-    { seconds: 15 }
13975
-];
13976
-var TimeGrid = /** @class */ (function (_super) {
13977
-    tslib_1.__extends(TimeGrid, _super);
13978
-    function TimeGrid(view) {
13979
-        var _this = _super.call(this, view) || this;
13980
-        _this.processOptions();
13981
-        return _this;
13982
-    }
13983
-    // Slices up the given span (unzoned start/end with other misc data) into an array of segments
13984
-    TimeGrid.prototype.componentFootprintToSegs = function (componentFootprint) {
13985
-        var segs = this.sliceRangeByTimes(componentFootprint.unzonedRange);
13986
-        var i;
13987
-        for (i = 0; i < segs.length; i++) {
13988
-            if (this.isRTL) {
13989
-                segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex;
13990
+        return new ComponentFootprint_1.default(new UnzonedRange_1.default(start, end), !start.hasTime());
13991
+    };
13992
+    // Date Utils
13993
+    // -----------------------------------------------------------------------------------------------------------------
13994
+    Calendar.prototype.initMomentInternals = function () {
13995
+        var _this = this;
13996
+        this.defaultAllDayEventDuration = moment.duration(this.opt('defaultAllDayEventDuration'));
13997
+        this.defaultTimedEventDuration = moment.duration(this.opt('defaultTimedEventDuration'));
13998
+        // Called immediately, and when any of the options change.
13999
+        // Happens before any internal objects rebuild or rerender, because this is very core.
14000
+        this.optionsManager.watch('buildingMomentLocale', [
14001
+            '?locale', '?monthNames', '?monthNamesShort', '?dayNames', '?dayNamesShort',
14002
+            '?firstDay', '?weekNumberCalculation'
14003
+        ], function (opts) {
14004
+            var weekNumberCalculation = opts.weekNumberCalculation;
14005
+            var firstDay = opts.firstDay;
14006
+            var _week;
14007
+            // normalize
14008
+            if (weekNumberCalculation === 'iso') {
14009
+                weekNumberCalculation = 'ISO'; // normalize
14010
             }
14011
-            else {
14012
-                segs[i].col = segs[i].dayIndex;
14013
+            var localeData = Object.create(// make a cheap copy
14014
+            locale_1.getMomentLocaleData(opts.locale) // will fall back to en
14015
+            );
14016
+            if (opts.monthNames) {
14017
+                localeData._months = opts.monthNames;
14018
             }
14019
-        }
14020
-        return segs;
14021
-    };
14022
-    /* Date Handling
14023
-    ------------------------------------------------------------------------------------------------------------------*/
14024
-    TimeGrid.prototype.sliceRangeByTimes = function (unzonedRange) {
14025
-        var segs = [];
14026
-        var segRange;
14027
-        var dayIndex;
14028
-        for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) {
14029
-            segRange = unzonedRange.intersect(this.dayRanges[dayIndex]);
14030
-            if (segRange) {
14031
-                segs.push({
14032
-                    startMs: segRange.startMs,
14033
-                    endMs: segRange.endMs,
14034
-                    isStart: segRange.isStart,
14035
-                    isEnd: segRange.isEnd,
14036
-                    dayIndex: dayIndex
14037
-                });
14038
+            if (opts.monthNamesShort) {
14039
+                localeData._monthsShort = opts.monthNamesShort;
14040
             }
14041
-        }
14042
-        return segs;
14043
-    };
14044
-    /* Options
14045
-    ------------------------------------------------------------------------------------------------------------------*/
14046
-    // Parses various options into properties of this object
14047
-    TimeGrid.prototype.processOptions = function () {
14048
-        var slotDuration = this.opt('slotDuration');
14049
-        var snapDuration = this.opt('snapDuration');
14050
-        var input;
14051
-        slotDuration = moment.duration(slotDuration);
14052
-        snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
14053
-        this.slotDuration = slotDuration;
14054
-        this.snapDuration = snapDuration;
14055
-        this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple?
14056
-        // might be an array value (for TimelineView).
14057
-        // if so, getting the most granular entry (the last one probably).
14058
-        input = this.opt('slotLabelFormat');
14059
-        if ($.isArray(input)) {
14060
-            input = input[input.length - 1];
14061
-        }
14062
-        this.labelFormat = input ||
14063
-            this.opt('smallTimeFormat'); // the computed default
14064
-        input = this.opt('slotLabelInterval');
14065
-        this.labelInterval = input ?
14066
-            moment.duration(input) :
14067
-            this.computeLabelInterval(slotDuration);
14068
+            if (opts.dayNames) {
14069
+                localeData._weekdays = opts.dayNames;
14070
+            }
14071
+            if (opts.dayNamesShort) {
14072
+                localeData._weekdaysShort = opts.dayNamesShort;
14073
+            }
14074
+            if (firstDay == null && weekNumberCalculation === 'ISO') {
14075
+                firstDay = 1;
14076
+            }
14077
+            if (firstDay != null) {
14078
+                _week = Object.create(localeData._week); // _week: { dow: # }
14079
+                _week.dow = firstDay;
14080
+                localeData._week = _week;
14081
+            }
14082
+            if ( // whitelist certain kinds of input
14083
+            weekNumberCalculation === 'ISO' ||
14084
+                weekNumberCalculation === 'local' ||
14085
+                typeof weekNumberCalculation === 'function') {
14086
+                localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it
14087
+            }
14088
+            _this.localeData = localeData;
14089
+            // If the internal current date object already exists, move to new locale.
14090
+            // We do NOT need to do this technique for event dates, because this happens when converting to "segments".
14091
+            if (_this.currentDate) {
14092
+                _this.localizeMoment(_this.currentDate); // sets to localeData
14093
+            }
14094
+        });
14095
     };
14096
-    // Computes an automatic value for slotLabelInterval
14097
-    TimeGrid.prototype.computeLabelInterval = function (slotDuration) {
14098
-        var i;
14099
-        var labelInterval;
14100
-        var slotsPerLabel;
14101
-        // find the smallest stock label interval that results in more than one slots-per-label
14102
-        for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) {
14103
-            labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]);
14104
-            slotsPerLabel = util_1.divideDurationByDuration(labelInterval, slotDuration);
14105
-            if (util_1.isInt(slotsPerLabel) && slotsPerLabel > 1) {
14106
-                return labelInterval;
14107
+    // Builds a moment using the settings of the current calendar: timezone and locale.
14108
+    // Accepts anything the vanilla moment() constructor accepts.
14109
+    Calendar.prototype.moment = function () {
14110
+        var args = [];
14111
+        for (var _i = 0; _i < arguments.length; _i++) {
14112
+            args[_i] = arguments[_i];
14113
+        }
14114
+        var mom;
14115
+        if (this.opt('timezone') === 'local') {
14116
+            mom = moment_ext_1.default.apply(null, args);
14117
+            // Force the moment to be local, because momentExt doesn't guarantee it.
14118
+            if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
14119
+                mom.local();
14120
             }
14121
         }
14122
-        return moment.duration(slotDuration); // fall back. clone
14123
+        else if (this.opt('timezone') === 'UTC') {
14124
+            mom = moment_ext_1.default.utc.apply(null, args); // process as UTC
14125
+        }
14126
+        else {
14127
+            mom = moment_ext_1.default.parseZone.apply(null, args); // let the input decide the zone
14128
+        }
14129
+        this.localizeMoment(mom); // TODO
14130
+        return mom;
14131
     };
14132
-    /* Date Rendering
14133
-    ------------------------------------------------------------------------------------------------------------------*/
14134
-    TimeGrid.prototype.renderDates = function (dateProfile) {
14135
-        this.dateProfile = dateProfile;
14136
-        this.updateDayTable();
14137
-        this.renderSlats();
14138
-        this.renderColumns();
14139
+    Calendar.prototype.msToMoment = function (ms, forceAllDay) {
14140
+        var mom = moment_ext_1.default.utc(ms); // TODO: optimize by using Date.UTC
14141
+        if (forceAllDay) {
14142
+            mom.stripTime();
14143
+        }
14144
+        else {
14145
+            mom = this.applyTimezone(mom); // may or may not apply locale
14146
+        }
14147
+        this.localizeMoment(mom);
14148
+        return mom;
14149
     };
14150
-    TimeGrid.prototype.unrenderDates = function () {
14151
-        // this.unrenderSlats(); // don't need this because repeated .html() calls clear
14152
-        this.unrenderColumns();
14153
+    Calendar.prototype.msToUtcMoment = function (ms, forceAllDay) {
14154
+        var mom = moment_ext_1.default.utc(ms); // TODO: optimize by using Date.UTC
14155
+        if (forceAllDay) {
14156
+            mom.stripTime();
14157
+        }
14158
+        this.localizeMoment(mom);
14159
+        return mom;
14160
     };
14161
-    TimeGrid.prototype.renderSkeleton = function () {
14162
-        var theme = this.view.calendar.theme;
14163
-        this.el.html('<div class="fc-bg"></div>' +
14164
-            '<div class="fc-slats"></div>' +
14165
-            '<hr class="fc-divider ' + theme.getClass('widgetHeader') + '" style="display:none" />');
14166
-        this.bottomRuleEl = this.el.find('hr');
14167
+    // Updates the given moment's locale settings to the current calendar locale settings.
14168
+    Calendar.prototype.localizeMoment = function (mom) {
14169
+        mom._locale = this.localeData;
14170
     };
14171
-    TimeGrid.prototype.renderSlats = function () {
14172
-        var theme = this.view.calendar.theme;
14173
-        this.slatContainerEl = this.el.find('> .fc-slats')
14174
-            .html(// avoids needing ::unrenderSlats()
14175
-        '<table class="' + theme.getClass('tableGrid') + '">' +
14176
-            this.renderSlatRowHtml() +
14177
-            '</table>');
14178
-        this.slatEls = this.slatContainerEl.find('tr');
14179
-        this.slatCoordCache = new CoordCache_1.default({
14180
-            els: this.slatEls,
14181
-            isVertical: true
14182
-        });
14183
+    // Returns a boolean about whether or not the calendar knows how to calculate
14184
+    // the timezone offset of arbitrary dates in the current timezone.
14185
+    Calendar.prototype.getIsAmbigTimezone = function () {
14186
+        return this.opt('timezone') !== 'local' && this.opt('timezone') !== 'UTC';
14187
     };
14188
-    // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
14189
-    TimeGrid.prototype.renderSlatRowHtml = function () {
14190
-        var view = this.view;
14191
-        var calendar = view.calendar;
14192
-        var theme = calendar.theme;
14193
-        var isRTL = this.isRTL;
14194
-        var dateProfile = this.dateProfile;
14195
-        var html = '';
14196
-        var slotTime = moment.duration(+dateProfile.minTime); // wish there was .clone() for durations
14197
-        var slotIterator = moment.duration(0);
14198
-        var slotDate; // will be on the view's first day, but we only care about its time
14199
-        var isLabeled;
14200
-        var axisHtml;
14201
-        // Calculate the time for each slot
14202
-        while (slotTime < dateProfile.maxTime) {
14203
-            slotDate = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.startMs).time(slotTime);
14204
-            isLabeled = util_1.isInt(util_1.divideDurationByDuration(slotIterator, this.labelInterval));
14205
-            axisHtml =
14206
-                '<td class="fc-axis fc-time ' + theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '>' +
14207
-                    (isLabeled ?
14208
-                        '<span>' + // for matchCellWidths
14209
-                            util_1.htmlEscape(slotDate.format(this.labelFormat)) +
14210
-                            '</span>' :
14211
-                        '') +
14212
-                    '</td>';
14213
-            html +=
14214
-                '<tr data-time="' + slotDate.format('HH:mm:ss') + '"' +
14215
-                    (isLabeled ? '' : ' class="fc-minor"') +
14216
-                    '>' +
14217
-                    (!isRTL ? axisHtml : '') +
14218
-                    '<td class="' + theme.getClass('widgetContent') + '"/>' +
14219
-                    (isRTL ? axisHtml : '') +
14220
-                    '</tr>';
14221
-            slotTime.add(this.slotDuration);
14222
-            slotIterator.add(this.slotDuration);
14223
+    // Returns a copy of the given date in the current timezone. Has no effect on dates without times.
14224
+    Calendar.prototype.applyTimezone = function (date) {
14225
+        if (!date.hasTime()) {
14226
+            return date.clone();
14227
         }
14228
-        return html;
14229
-    };
14230
-    TimeGrid.prototype.renderColumns = function () {
14231
-        var dateProfile = this.dateProfile;
14232
-        var theme = this.view.calendar.theme;
14233
-        this.dayRanges = this.dayDates.map(function (dayDate) {
14234
-            return new UnzonedRange_1.default(dayDate.clone().add(dateProfile.minTime), dayDate.clone().add(dateProfile.maxTime));
14235
-        });
14236
-        if (this.headContainerEl) {
14237
-            this.headContainerEl.html(this.renderHeadHtml());
14238
+        var zonedDate = this.moment(date.toArray());
14239
+        var timeAdjust = date.time().asMilliseconds() - zonedDate.time().asMilliseconds();
14240
+        var adjustedZonedDate;
14241
+        // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396)
14242
+        if (timeAdjust) { // is the time result different than expected?
14243
+            adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds
14244
+            if (date.time().asMilliseconds() - adjustedZonedDate.time().asMilliseconds() === 0) { // does it match perfectly now?
14245
+                zonedDate = adjustedZonedDate;
14246
+            }
14247
         }
14248
-        this.el.find('> .fc-bg').html('<table class="' + theme.getClass('tableGrid') + '">' +
14249
-            this.renderBgTrHtml(0) + // row=0
14250
-            '</table>');
14251
-        this.colEls = this.el.find('.fc-day, .fc-disabled-day');
14252
-        this.colCoordCache = new CoordCache_1.default({
14253
-            els: this.colEls,
14254
-            isHorizontal: true
14255
-        });
14256
-        this.renderContentSkeleton();
14257
-    };
14258
-    TimeGrid.prototype.unrenderColumns = function () {
14259
-        this.unrenderContentSkeleton();
14260
+        return zonedDate;
14261
     };
14262
-    /* Content Skeleton
14263
-    ------------------------------------------------------------------------------------------------------------------*/
14264
-    // Renders the DOM that the view's content will live in
14265
-    TimeGrid.prototype.renderContentSkeleton = function () {
14266
-        var cellHtml = '';
14267
-        var i;
14268
-        var skeletonEl;
14269
-        for (i = 0; i < this.colCnt; i++) {
14270
-            cellHtml +=
14271
-                '<td>' +
14272
-                    '<div class="fc-content-col">' +
14273
-                    '<div class="fc-event-container fc-helper-container"></div>' +
14274
-                    '<div class="fc-event-container"></div>' +
14275
-                    '<div class="fc-highlight-container"></div>' +
14276
-                    '<div class="fc-bgevent-container"></div>' +
14277
-                    '<div class="fc-business-container"></div>' +
14278
-                    '</div>' +
14279
-                    '</td>';
14280
+    /*
14281
+    Assumes the footprint is non-open-ended.
14282
+    */
14283
+    Calendar.prototype.footprintToDateProfile = function (componentFootprint, ignoreEnd) {
14284
+        if (ignoreEnd === void 0) { ignoreEnd = false; }
14285
+        var start = moment_ext_1.default.utc(componentFootprint.unzonedRange.startMs);
14286
+        var end;
14287
+        if (!ignoreEnd) {
14288
+            end = moment_ext_1.default.utc(componentFootprint.unzonedRange.endMs);
14289
         }
14290
-        skeletonEl = this.contentSkeletonEl = $('<div class="fc-content-skeleton">' +
14291
-            '<table>' +
14292
-            '<tr>' + cellHtml + '</tr>' +
14293
-            '</table>' +
14294
-            '</div>');
14295
-        this.colContainerEls = skeletonEl.find('.fc-content-col');
14296
-        this.helperContainerEls = skeletonEl.find('.fc-helper-container');
14297
-        this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)');
14298
-        this.bgContainerEls = skeletonEl.find('.fc-bgevent-container');
14299
-        this.highlightContainerEls = skeletonEl.find('.fc-highlight-container');
14300
-        this.businessContainerEls = skeletonEl.find('.fc-business-container');
14301
-        this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level
14302
-        this.el.append(skeletonEl);
14303
-    };
14304
-    TimeGrid.prototype.unrenderContentSkeleton = function () {
14305
-        if (this.contentSkeletonEl) {
14306
-            this.contentSkeletonEl.remove();
14307
-            this.contentSkeletonEl = null;
14308
-            this.colContainerEls = null;
14309
-            this.helperContainerEls = null;
14310
-            this.fgContainerEls = null;
14311
-            this.bgContainerEls = null;
14312
-            this.highlightContainerEls = null;
14313
-            this.businessContainerEls = null;
14314
+        if (componentFootprint.isAllDay) {
14315
+            start.stripTime();
14316
+            if (end) {
14317
+                end.stripTime();
14318
+            }
14319
         }
14320
-    };
14321
-    // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
14322
-    TimeGrid.prototype.groupSegsByCol = function (segs) {
14323
-        var segsByCol = [];
14324
-        var i;
14325
-        for (i = 0; i < this.colCnt; i++) {
14326
-            segsByCol.push([]);
14327
+        else {
14328
+            start = this.applyTimezone(start);
14329
+            if (end) {
14330
+                end = this.applyTimezone(end);
14331
+            }
14332
         }
14333
-        for (i = 0; i < segs.length; i++) {
14334
-            segsByCol[segs[i].col].push(segs[i]);
14335
+        this.localizeMoment(start);
14336
+        if (end) {
14337
+            this.localizeMoment(end);
14338
         }
14339
-        return segsByCol;
14340
+        return new EventDateProfile_1.default(start, end, this);
14341
     };
14342
-    // Given segments grouped by column, insert the segments' elements into a parallel array of container
14343
-    // elements, each living within a column.
14344
-    TimeGrid.prototype.attachSegsByCol = function (segsByCol, containerEls) {
14345
-        var col;
14346
-        var segs;
14347
-        var i;
14348
-        for (col = 0; col < this.colCnt; col++) {
14349
-            segs = segsByCol[col];
14350
-            for (i = 0; i < segs.length; i++) {
14351
-                containerEls.eq(col).append(segs[i].el);
14352
-            }
14353
+    // Returns a moment for the current date, as defined by the client's computer or from the `now` option.
14354
+    // Will return an moment with an ambiguous timezone.
14355
+    Calendar.prototype.getNow = function () {
14356
+        var now = this.opt('now');
14357
+        if (typeof now === 'function') {
14358
+            now = now();
14359
         }
14360
+        return this.moment(now).stripZone();
14361
     };
14362
-    /* Now Indicator
14363
-    ------------------------------------------------------------------------------------------------------------------*/
14364
-    TimeGrid.prototype.getNowIndicatorUnit = function () {
14365
-        return 'minute'; // will refresh on the minute
14366
+    // Produces a human-readable string for the given duration.
14367
+    // Side-effect: changes the locale of the given duration.
14368
+    Calendar.prototype.humanizeDuration = function (duration) {
14369
+        return duration.locale(this.opt('locale')).humanize();
14370
     };
14371
-    TimeGrid.prototype.renderNowIndicator = function (date) {
14372
-        // HACK: if date columns not ready for some reason (scheduler)
14373
-        if (!this.colContainerEls) {
14374
-            return;
14375
+    // will return `null` if invalid range
14376
+    Calendar.prototype.parseUnzonedRange = function (rangeInput) {
14377
+        var start = null;
14378
+        var end = null;
14379
+        if (rangeInput.start) {
14380
+            start = this.moment(rangeInput.start).stripZone();
14381
         }
14382
-        // seg system might be overkill, but it handles scenario where line needs to be rendered
14383
-        //  more than once because of columns with the same date (resources columns for example)
14384
-        var segs = this.componentFootprintToSegs(new ComponentFootprint_1.default(new UnzonedRange_1.default(date, date.valueOf() + 1), // protect against null range
14385
-        false // all-day
14386
-        ));
14387
-        var top = this.computeDateTop(date, date);
14388
-        var nodes = [];
14389
-        var i;
14390
-        // render lines within the columns
14391
-        for (i = 0; i < segs.length; i++) {
14392
-            nodes.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>')
14393
-                .css('top', top)
14394
-                .appendTo(this.colContainerEls.eq(segs[i].col))[0]);
14395
+        if (rangeInput.end) {
14396
+            end = this.moment(rangeInput.end).stripZone();
14397
         }
14398
-        // render an arrow over the axis
14399
-        if (segs.length > 0) {
14400
-            nodes.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>')
14401
-                .css('top', top)
14402
-                .appendTo(this.el.find('.fc-content-skeleton'))[0]);
14403
+        if (!start && !end) {
14404
+            return null;
14405
         }
14406
-        this.nowIndicatorEls = $(nodes);
14407
+        if (start && end && end.isBefore(start)) {
14408
+            return null;
14409
+        }
14410
+        return new UnzonedRange_1.default(start, end);
14411
     };
14412
-    TimeGrid.prototype.unrenderNowIndicator = function () {
14413
-        if (this.nowIndicatorEls) {
14414
-            this.nowIndicatorEls.remove();
14415
-            this.nowIndicatorEls = null;
14416
+    // Event-Date Utilities
14417
+    // -----------------------------------------------------------------------------------------------------------------
14418
+    Calendar.prototype.initEventManager = function () {
14419
+        var _this = this;
14420
+        var eventManager = new EventManager_1.default(this);
14421
+        var rawSources = this.opt('eventSources') || [];
14422
+        var singleRawSource = this.opt('events');
14423
+        this.eventManager = eventManager;
14424
+        if (singleRawSource) {
14425
+            rawSources.unshift(singleRawSource);
14426
         }
14427
+        eventManager.on('release', function (eventsPayload) {
14428
+            _this.trigger('eventsReset', eventsPayload);
14429
+        });
14430
+        eventManager.freeze();
14431
+        rawSources.forEach(function (rawSource) {
14432
+            var source = EventSourceParser_1.default.parse(rawSource, _this);
14433
+            if (source) {
14434
+                eventManager.addSource(source);
14435
+            }
14436
+        });
14437
+        eventManager.thaw();
14438
     };
14439
-    /* Coordinates
14440
-    ------------------------------------------------------------------------------------------------------------------*/
14441
-    TimeGrid.prototype.updateSize = function (totalHeight, isAuto, isResize) {
14442
-        _super.prototype.updateSize.call(this, totalHeight, isAuto, isResize);
14443
-        this.slatCoordCache.build();
14444
-        if (isResize) {
14445
-            this.updateSegVerticals([].concat(this.eventRenderer.getSegs(), this.businessSegs || []));
14446
+    Calendar.prototype.requestEvents = function (start, end) {
14447
+        return this.eventManager.requestEvents(start, end, this.opt('timezone'), !this.opt('lazyFetching'));
14448
+    };
14449
+    // Get an event's normalized end date. If not present, calculate it from the defaults.
14450
+    Calendar.prototype.getEventEnd = function (event) {
14451
+        if (event.end) {
14452
+            return event.end.clone();
14453
+        }
14454
+        else {
14455
+            return this.getDefaultEventEnd(event.allDay, event.start);
14456
         }
14457
     };
14458
-    TimeGrid.prototype.getTotalSlatHeight = function () {
14459
-        return this.slatContainerEl.outerHeight();
14460
+    // Given an event's allDay status and start date, return what its fallback end date should be.
14461
+    // TODO: rename to computeDefaultEventEnd
14462
+    Calendar.prototype.getDefaultEventEnd = function (allDay, zonedStart) {
14463
+        var end = zonedStart.clone();
14464
+        if (allDay) {
14465
+            end.stripTime().add(this.defaultAllDayEventDuration);
14466
+        }
14467
+        else {
14468
+            end.add(this.defaultTimedEventDuration);
14469
+        }
14470
+        if (this.getIsAmbigTimezone()) {
14471
+            end.stripZone(); // we don't know what the tzo should be
14472
+        }
14473
+        return end;
14474
     };
14475
-    // Computes the top coordinate, relative to the bounds of the grid, of the given date.
14476
-    // `ms` can be a millisecond UTC time OR a UTC moment.
14477
-    // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
14478
-    TimeGrid.prototype.computeDateTop = function (ms, startOfDayDate) {
14479
-        return this.computeTimeTop(moment.duration(ms - startOfDayDate.clone().stripTime()));
14480
+    // Public Events API
14481
+    // -----------------------------------------------------------------------------------------------------------------
14482
+    Calendar.prototype.rerenderEvents = function () {
14483
+        this.view.flash('displayingEvents');
14484
     };
14485
-    // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
14486
-    TimeGrid.prototype.computeTimeTop = function (time) {
14487
-        var len = this.slatEls.length;
14488
-        var dateProfile = this.dateProfile;
14489
-        var slatCoverage = (time - dateProfile.minTime) / this.slotDuration; // floating-point value of # of slots covered
14490
-        var slatIndex;
14491
-        var slatRemainder;
14492
-        // compute a floating-point number for how many slats should be progressed through.
14493
-        // from 0 to number of slats (inclusive)
14494
-        // constrained because minTime/maxTime might be customized.
14495
-        slatCoverage = Math.max(0, slatCoverage);
14496
-        slatCoverage = Math.min(len, slatCoverage);
14497
-        // an integer index of the furthest whole slat
14498
-        // from 0 to number slats (*exclusive*, so len-1)
14499
-        slatIndex = Math.floor(slatCoverage);
14500
-        slatIndex = Math.min(slatIndex, len - 1);
14501
-        // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
14502
-        // could be 1.0 if slatCoverage is covering *all* the slots
14503
-        slatRemainder = slatCoverage - slatIndex;
14504
-        return this.slatCoordCache.getTopPosition(slatIndex) +
14505
-            this.slatCoordCache.getHeight(slatIndex) * slatRemainder;
14506
+    Calendar.prototype.refetchEvents = function () {
14507
+        this.eventManager.refetchAllSources();
14508
     };
14509
-    // Refreshes the CSS top/bottom coordinates for each segment element.
14510
-    // Works when called after initial render, after a window resize/zoom for example.
14511
-    TimeGrid.prototype.updateSegVerticals = function (segs) {
14512
-        this.computeSegVerticals(segs);
14513
-        this.assignSegVerticals(segs);
14514
+    Calendar.prototype.renderEvents = function (eventInputs, isSticky) {
14515
+        this.eventManager.freeze();
14516
+        for (var i = 0; i < eventInputs.length; i++) {
14517
+            this.renderEvent(eventInputs[i], isSticky);
14518
+        }
14519
+        this.eventManager.thaw();
14520
     };
14521
-    // For each segment in an array, computes and assigns its top and bottom properties
14522
-    TimeGrid.prototype.computeSegVerticals = function (segs) {
14523
-        var eventMinHeight = this.opt('agendaEventMinHeight');
14524
-        var i;
14525
-        var seg;
14526
-        var dayDate;
14527
-        for (i = 0; i < segs.length; i++) {
14528
-            seg = segs[i];
14529
-            dayDate = this.dayDates[seg.dayIndex];
14530
-            seg.top = this.computeDateTop(seg.startMs, dayDate);
14531
-            seg.bottom = Math.max(seg.top + eventMinHeight, this.computeDateTop(seg.endMs, dayDate));
14532
+    Calendar.prototype.renderEvent = function (eventInput, isSticky) {
14533
+        if (isSticky === void 0) { isSticky = false; }
14534
+        var eventManager = this.eventManager;
14535
+        var eventDef = EventDefParser_1.default.parse(eventInput, eventInput.source || eventManager.stickySource);
14536
+        if (eventDef) {
14537
+            eventManager.addEventDef(eventDef, isSticky);
14538
         }
14539
     };
14540
-    // Given segments that already have their top/bottom properties computed, applies those values to
14541
-    // the segments' elements.
14542
-    TimeGrid.prototype.assignSegVerticals = function (segs) {
14543
+    // legacyQuery operates on legacy event instance objects
14544
+    Calendar.prototype.removeEvents = function (legacyQuery) {
14545
+        var eventManager = this.eventManager;
14546
+        var legacyInstances = [];
14547
+        var idMap = {};
14548
+        var eventDef;
14549
         var i;
14550
-        var seg;
14551
-        for (i = 0; i < segs.length; i++) {
14552
-            seg = segs[i];
14553
-            seg.el.css(this.generateSegVerticalCss(seg));
14554
+        if (legacyQuery == null) { // shortcut for removing all
14555
+            eventManager.removeAllEventDefs(); // persist=true
14556
+        }
14557
+        else {
14558
+            eventManager.getEventInstances().forEach(function (eventInstance) {
14559
+                legacyInstances.push(eventInstance.toLegacy());
14560
+            });
14561
+            legacyInstances = filterLegacyEventInstances(legacyInstances, legacyQuery);
14562
+            // compute unique IDs
14563
+            for (i = 0; i < legacyInstances.length; i++) {
14564
+                eventDef = this.eventManager.getEventDefByUid(legacyInstances[i]._id);
14565
+                idMap[eventDef.id] = true;
14566
+            }
14567
+            eventManager.freeze();
14568
+            for (i in idMap) { // reuse `i` as an "id"
14569
+                eventManager.removeEventDefsById(i); // persist=true
14570
+            }
14571
+            eventManager.thaw();
14572
         }
14573
     };
14574
-    // Generates an object with CSS properties for the top/bottom coordinates of a segment element
14575
-    TimeGrid.prototype.generateSegVerticalCss = function (seg) {
14576
-        return {
14577
-            top: seg.top,
14578
-            bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
14579
-        };
14580
-    };
14581
-    /* Hit System
14582
-    ------------------------------------------------------------------------------------------------------------------*/
14583
-    TimeGrid.prototype.prepareHits = function () {
14584
-        this.colCoordCache.build();
14585
-        this.slatCoordCache.build();
14586
-    };
14587
-    TimeGrid.prototype.releaseHits = function () {
14588
-        this.colCoordCache.clear();
14589
-        // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop
14590
+    // legacyQuery operates on legacy event instance objects
14591
+    Calendar.prototype.clientEvents = function (legacyQuery) {
14592
+        var legacyEventInstances = [];
14593
+        this.eventManager.getEventInstances().forEach(function (eventInstance) {
14594
+            legacyEventInstances.push(eventInstance.toLegacy());
14595
+        });
14596
+        return filterLegacyEventInstances(legacyEventInstances, legacyQuery);
14597
     };
14598
-    TimeGrid.prototype.queryHit = function (leftOffset, topOffset) {
14599
-        var snapsPerSlot = this.snapsPerSlot;
14600
-        var colCoordCache = this.colCoordCache;
14601
-        var slatCoordCache = this.slatCoordCache;
14602
-        if (colCoordCache.isLeftInBounds(leftOffset) && slatCoordCache.isTopInBounds(topOffset)) {
14603
-            var colIndex = colCoordCache.getHorizontalIndex(leftOffset);
14604
-            var slatIndex = slatCoordCache.getVerticalIndex(topOffset);
14605
-            if (colIndex != null && slatIndex != null) {
14606
-                var slatTop = slatCoordCache.getTopOffset(slatIndex);
14607
-                var slatHeight = slatCoordCache.getHeight(slatIndex);
14608
-                var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1
14609
-                var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat
14610
-                var snapIndex = slatIndex * snapsPerSlot + localSnapIndex;
14611
-                var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight;
14612
-                var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight;
14613
-                return {
14614
-                    col: colIndex,
14615
-                    snap: snapIndex,
14616
-                    component: this,
14617
-                    left: colCoordCache.getLeftOffset(colIndex),
14618
-                    right: colCoordCache.getRightOffset(colIndex),
14619
-                    top: snapTop,
14620
-                    bottom: snapBottom
14621
-                };
14622
-            }
14623
+    Calendar.prototype.updateEvents = function (eventPropsArray) {
14624
+        this.eventManager.freeze();
14625
+        for (var i = 0; i < eventPropsArray.length; i++) {
14626
+            this.updateEvent(eventPropsArray[i]);
14627
         }
14628
+        this.eventManager.thaw();
14629
     };
14630
-    TimeGrid.prototype.getHitFootprint = function (hit) {
14631
-        var start = this.getCellDate(0, hit.col); // row=0
14632
-        var time = this.computeSnapTime(hit.snap); // pass in the snap-index
14633
-        var end;
14634
-        start.time(time);
14635
-        end = start.clone().add(this.snapDuration);
14636
-        return new ComponentFootprint_1.default(new UnzonedRange_1.default(start, end), false // all-day?
14637
-        );
14638
+    Calendar.prototype.updateEvent = function (eventProps) {
14639
+        var eventDef = this.eventManager.getEventDefByUid(eventProps._id);
14640
+        var eventInstance;
14641
+        var eventDefMutation;
14642
+        if (eventDef instanceof SingleEventDef_1.default) {
14643
+            eventInstance = eventDef.buildInstance();
14644
+            eventDefMutation = EventDefMutation_1.default.createFromRawProps(eventInstance, eventProps, // raw props
14645
+            null // largeUnit -- who uses it?
14646
+            );
14647
+            this.eventManager.mutateEventsWithId(eventDef.id, eventDefMutation); // will release
14648
+        }
14649
     };
14650
-    // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
14651
-    TimeGrid.prototype.computeSnapTime = function (snapIndex) {
14652
-        return moment.duration(this.dateProfile.minTime + this.snapDuration * snapIndex);
14653
+    // Public Event Sources API
14654
+    // ------------------------------------------------------------------------------------
14655
+    Calendar.prototype.getEventSources = function () {
14656
+        return this.eventManager.otherSources.slice(); // clone
14657
     };
14658
-    TimeGrid.prototype.getHitEl = function (hit) {
14659
-        return this.colEls.eq(hit.col);
14660
+    Calendar.prototype.getEventSourceById = function (id) {
14661
+        return this.eventManager.getSourceById(EventSource_1.default.normalizeId(id));
14662
     };
14663
-    /* Event Drag Visualization
14664
-    ------------------------------------------------------------------------------------------------------------------*/
14665
-    // Renders a visual indication of an event being dragged over the specified date(s).
14666
-    // A returned value of `true` signals that a mock "helper" event has been rendered.
14667
-    TimeGrid.prototype.renderDrag = function (eventFootprints, seg, isTouch) {
14668
+    Calendar.prototype.addEventSource = function (sourceInput) {
14669
+        var source = EventSourceParser_1.default.parse(sourceInput, this);
14670
+        if (source) {
14671
+            this.eventManager.addSource(source);
14672
+        }
14673
+    };
14674
+    Calendar.prototype.removeEventSources = function (sourceMultiQuery) {
14675
+        var eventManager = this.eventManager;
14676
+        var sources;
14677
         var i;
14678
-        if (seg) {
14679
-            if (eventFootprints.length) {
14680
-                this.helperRenderer.renderEventDraggingFootprints(eventFootprints, seg, isTouch);
14681
-                // signal that a helper has been rendered
14682
-                return true;
14683
-            }
14684
+        if (sourceMultiQuery == null) {
14685
+            this.eventManager.removeAllSources();
14686
         }
14687
         else {
14688
-            for (i = 0; i < eventFootprints.length; i++) {
14689
-                this.renderHighlight(eventFootprints[i].componentFootprint);
14690
+            sources = eventManager.multiQuerySources(sourceMultiQuery);
14691
+            eventManager.freeze();
14692
+            for (i = 0; i < sources.length; i++) {
14693
+                eventManager.removeSource(sources[i]);
14694
             }
14695
+            eventManager.thaw();
14696
         }
14697
     };
14698
-    // Unrenders any visual indication of an event being dragged
14699
-    TimeGrid.prototype.unrenderDrag = function () {
14700
-        this.unrenderHighlight();
14701
-        this.helperRenderer.unrender();
14702
-    };
14703
-    /* Event Resize Visualization
14704
-    ------------------------------------------------------------------------------------------------------------------*/
14705
-    // Renders a visual indication of an event being resized
14706
-    TimeGrid.prototype.renderEventResize = function (eventFootprints, seg, isTouch) {
14707
-        this.helperRenderer.renderEventResizingFootprints(eventFootprints, seg, isTouch);
14708
-    };
14709
-    // Unrenders any visual indication of an event being resized
14710
-    TimeGrid.prototype.unrenderEventResize = function () {
14711
-        this.helperRenderer.unrender();
14712
-    };
14713
-    /* Selection
14714
-    ------------------------------------------------------------------------------------------------------------------*/
14715
-    // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
14716
-    TimeGrid.prototype.renderSelectionFootprint = function (componentFootprint) {
14717
-        if (this.opt('selectHelper')) {
14718
-            this.helperRenderer.renderComponentFootprint(componentFootprint);
14719
-        }
14720
-        else {
14721
-            this.renderHighlight(componentFootprint);
14722
+    Calendar.prototype.removeEventSource = function (sourceQuery) {
14723
+        var eventManager = this.eventManager;
14724
+        var sources = eventManager.querySources(sourceQuery);
14725
+        var i;
14726
+        eventManager.freeze();
14727
+        for (i = 0; i < sources.length; i++) {
14728
+            eventManager.removeSource(sources[i]);
14729
         }
14730
+        eventManager.thaw();
14731
     };
14732
-    // Unrenders any visual indication of a selection
14733
-    TimeGrid.prototype.unrenderSelection = function () {
14734
-        this.helperRenderer.unrender();
14735
-        this.unrenderHighlight();
14736
+    Calendar.prototype.refetchEventSources = function (sourceMultiQuery) {
14737
+        var eventManager = this.eventManager;
14738
+        var sources = eventManager.multiQuerySources(sourceMultiQuery);
14739
+        var i;
14740
+        eventManager.freeze();
14741
+        for (i = 0; i < sources.length; i++) {
14742
+            eventManager.refetchSource(sources[i]);
14743
+        }
14744
+        eventManager.thaw();
14745
     };
14746
-    return TimeGrid;
14747
-}(InteractiveDateComponent_1.default));
14748
-exports.default = TimeGrid;
14749
-TimeGrid.prototype.eventRendererClass = TimeGridEventRenderer_1.default;
14750
-TimeGrid.prototype.businessHourRendererClass = BusinessHourRenderer_1.default;
14751
-TimeGrid.prototype.helperRendererClass = TimeGridHelperRenderer_1.default;
14752
-TimeGrid.prototype.fillRendererClass = TimeGridFillRenderer_1.default;
14753
-StandardInteractionsMixin_1.default.mixInto(TimeGrid);
14754
-DayTableMixin_1.default.mixInto(TimeGrid);
14755
+    // not for internal use. use options module directly instead.
14756
+    Calendar.defaults = options_1.globalDefaults;
14757
+    Calendar.englishDefaults = options_1.englishDefaults;
14758
+    Calendar.rtlDefaults = options_1.rtlDefaults;
14759
+    return Calendar;
14760
+}());
14761
+exports.default = Calendar;
14762
+EmitterMixin_1.default.mixInto(Calendar);
14763
+ListenerMixin_1.default.mixInto(Calendar);
14764
+function filterLegacyEventInstances(legacyEventInstances, legacyQuery) {
14765
+    if (legacyQuery == null) {
14766
+        return legacyEventInstances;
14767
+    }
14768
+    else if ($.isFunction(legacyQuery)) {
14769
+        return legacyEventInstances.filter(legacyQuery);
14770
+    }
14771
+    else { // an event ID
14772
+        legacyQuery += ''; // normalize to string
14773
+        return legacyEventInstances.filter(function (legacyEventInstance) {
14774
+            // soft comparison because id not be normalized to string
14775
+            // tslint:disable-next-line
14776
+            return legacyEventInstance.id == legacyQuery ||
14777
+                legacyEventInstance._id === legacyQuery; // can specify internal id, but must exactly match
14778
+        });
14779
+    }
14780
+}
14781
 
14782
 
14783
 /***/ }),
14784
-/* 228 */
14785
+/* 233 */
14786
 /***/ (function(module, exports, __webpack_require__) {
14787
 
14788
 Object.defineProperty(exports, "__esModule", { value: true });
14789
 var tslib_1 = __webpack_require__(2);
14790
-var UnzonedRange_1 = __webpack_require__(5);
14791
-var DateProfileGenerator_1 = __webpack_require__(221);
14792
-var BasicViewDateProfileGenerator = /** @class */ (function (_super) {
14793
-    tslib_1.__extends(BasicViewDateProfileGenerator, _super);
14794
-    function BasicViewDateProfileGenerator() {
14795
-        return _super !== null && _super.apply(this, arguments) || this;
14796
+var $ = __webpack_require__(3);
14797
+var moment = __webpack_require__(0);
14798
+var exportHooks = __webpack_require__(18);
14799
+var util_1 = __webpack_require__(4);
14800
+var moment_ext_1 = __webpack_require__(11);
14801
+var ListenerMixin_1 = __webpack_require__(7);
14802
+var HitDragListener_1 = __webpack_require__(17);
14803
+var SingleEventDef_1 = __webpack_require__(9);
14804
+var EventInstanceGroup_1 = __webpack_require__(20);
14805
+var EventSource_1 = __webpack_require__(6);
14806
+var Interaction_1 = __webpack_require__(14);
14807
+var ExternalDropping = /** @class */ (function (_super) {
14808
+    tslib_1.__extends(ExternalDropping, _super);
14809
+    function ExternalDropping() {
14810
+        var _this = _super !== null && _super.apply(this, arguments) || this;
14811
+        _this.isDragging = false; // jqui-dragging an external element? boolean
14812
+        return _this;
14813
     }
14814
-    // Computes the date range that will be rendered.
14815
-    BasicViewDateProfileGenerator.prototype.buildRenderRange = function (currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
14816
-        var renderUnzonedRange = _super.prototype.buildRenderRange.call(this, currentUnzonedRange, currentRangeUnit, isRangeAllDay); // an UnzonedRange
14817
-        var start = this.msToUtcMoment(renderUnzonedRange.startMs, isRangeAllDay);
14818
-        var end = this.msToUtcMoment(renderUnzonedRange.endMs, isRangeAllDay);
14819
-        // year and month views should be aligned with weeks. this is already done for week
14820
-        if (/^(year|month)$/.test(currentRangeUnit)) {
14821
-            start.startOf('week');
14822
-            // make end-of-week if not already
14823
-            if (end.weekday()) {
14824
-                end.add(1, 'week').startOf('week'); // exclusively move backwards
14825
+    /*
14826
+    component impements:
14827
+      - eventRangesToEventFootprints
14828
+      - isEventInstanceGroupAllowed
14829
+      - isExternalInstanceGroupAllowed
14830
+      - renderDrag
14831
+      - unrenderDrag
14832
+    */
14833
+    ExternalDropping.prototype.end = function () {
14834
+        if (this.dragListener) {
14835
+            this.dragListener.endInteraction();
14836
+        }
14837
+    };
14838
+    ExternalDropping.prototype.bindToDocument = function () {
14839
+        this.listenTo($(document), {
14840
+            dragstart: this.handleDragStart,
14841
+            sortstart: this.handleDragStart // jqui
14842
+        });
14843
+    };
14844
+    ExternalDropping.prototype.unbindFromDocument = function () {
14845
+        this.stopListeningTo($(document));
14846
+    };
14847
+    // Called when a jQuery UI drag is initiated anywhere in the DOM
14848
+    ExternalDropping.prototype.handleDragStart = function (ev, ui) {
14849
+        var el;
14850
+        var accept;
14851
+        if (this.opt('droppable')) { // only listen if this setting is on
14852
+            el = $((ui ? ui.item : null) || ev.target);
14853
+            // Test that the dragged element passes the dropAccept selector or filter function.
14854
+            // FYI, the default is "*" (matches all)
14855
+            accept = this.opt('dropAccept');
14856
+            if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
14857
+                if (!this.isDragging) { // prevent double-listening if fired twice
14858
+                    this.listenToExternalDrag(el, ev, ui);
14859
+                }
14860
+            }
14861
+        }
14862
+    };
14863
+    // Called when a jQuery UI drag starts and it needs to be monitored for dropping
14864
+    ExternalDropping.prototype.listenToExternalDrag = function (el, ev, ui) {
14865
+        var _this = this;
14866
+        var component = this.component;
14867
+        var view = this.view;
14868
+        var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
14869
+        var singleEventDef; // a null value signals an unsuccessful drag
14870
+        // listener that tracks mouse movement over date-associated pixel regions
14871
+        var dragListener = this.dragListener = new HitDragListener_1.default(component, {
14872
+            interactionStart: function () {
14873
+                _this.isDragging = true;
14874
+            },
14875
+            hitOver: function (hit) {
14876
+                var isAllowed = true;
14877
+                var hitFootprint = hit.component.getSafeHitFootprint(hit); // hit might not belong to this grid
14878
+                var mutatedEventInstanceGroup;
14879
+                if (hitFootprint) {
14880
+                    singleEventDef = _this.computeExternalDrop(hitFootprint, meta);
14881
+                    if (singleEventDef) {
14882
+                        mutatedEventInstanceGroup = new EventInstanceGroup_1.default(singleEventDef.buildInstances());
14883
+                        isAllowed = meta.eventProps ? // isEvent?
14884
+                            component.isEventInstanceGroupAllowed(mutatedEventInstanceGroup) :
14885
+                            component.isExternalInstanceGroupAllowed(mutatedEventInstanceGroup);
14886
+                    }
14887
+                    else {
14888
+                        isAllowed = false;
14889
+                    }
14890
+                }
14891
+                else {
14892
+                    isAllowed = false;
14893
+                }
14894
+                if (!isAllowed) {
14895
+                    singleEventDef = null;
14896
+                    util_1.disableCursor();
14897
+                }
14898
+                if (singleEventDef) {
14899
+                    component.renderDrag(// called without a seg parameter
14900
+                    component.eventRangesToEventFootprints(mutatedEventInstanceGroup.sliceRenderRanges(component.dateProfile.renderUnzonedRange, view.calendar)));
14901
+                }
14902
+            },
14903
+            hitOut: function () {
14904
+                singleEventDef = null; // signal unsuccessful
14905
+            },
14906
+            hitDone: function () {
14907
+                util_1.enableCursor();
14908
+                component.unrenderDrag();
14909
+            },
14910
+            interactionEnd: function (ev) {
14911
+                if (singleEventDef) { // element was dropped on a valid hit
14912
+                    view.reportExternalDrop(singleEventDef, Boolean(meta.eventProps), // isEvent
14913
+                    Boolean(meta.stick), // isSticky
14914
+                    el, ev, ui);
14915
+                }
14916
+                _this.isDragging = false;
14917
+                _this.dragListener = null;
14918
+            }
14919
+        });
14920
+        dragListener.startDrag(ev); // start listening immediately
14921
+    };
14922
+    // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
14923
+    // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
14924
+    // Returning a null value signals an invalid drop hit.
14925
+    // DOES NOT consider overlap/constraint.
14926
+    // Assumes both footprints are non-open-ended.
14927
+    ExternalDropping.prototype.computeExternalDrop = function (componentFootprint, meta) {
14928
+        var calendar = this.view.calendar;
14929
+        var start = moment_ext_1.default.utc(componentFootprint.unzonedRange.startMs).stripZone();
14930
+        var end;
14931
+        var eventDef;
14932
+        if (componentFootprint.isAllDay) {
14933
+            // if dropped on an all-day span, and element's metadata specified a time, set it
14934
+            if (meta.startTime) {
14935
+                start.time(meta.startTime);
14936
+            }
14937
+            else {
14938
+                start.stripTime();
14939
             }
14940
         }
14941
-        return new UnzonedRange_1.default(start, end);
14942
+        if (meta.duration) {
14943
+            end = start.clone().add(meta.duration);
14944
+        }
14945
+        start = calendar.applyTimezone(start);
14946
+        if (end) {
14947
+            end = calendar.applyTimezone(end);
14948
+        }
14949
+        eventDef = SingleEventDef_1.default.parse($.extend({}, meta.eventProps, {
14950
+            start: start,
14951
+            end: end
14952
+        }), new EventSource_1.default(calendar));
14953
+        return eventDef;
14954
     };
14955
-    return BasicViewDateProfileGenerator;
14956
-}(DateProfileGenerator_1.default));
14957
-exports.default = BasicViewDateProfileGenerator;
14958
+    return ExternalDropping;
14959
+}(Interaction_1.default));
14960
+exports.default = ExternalDropping;
14961
+ListenerMixin_1.default.mixInto(ExternalDropping);
14962
+/* External-Dragging-Element Data
14963
+----------------------------------------------------------------------------------------------------------------------*/
14964
+// Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
14965
+// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
14966
+exportHooks.dataAttrPrefix = '';
14967
+// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
14968
+// to be used for Event Object creation.
14969
+// A defined `.eventProps`, even when empty, indicates that an event should be created.
14970
+function getDraggedElMeta(el) {
14971
+    var prefix = exportHooks.dataAttrPrefix;
14972
+    var eventProps; // properties for creating the event, not related to date/time
14973
+    var startTime; // a Duration
14974
+    var duration;
14975
+    var stick;
14976
+    if (prefix) {
14977
+        prefix += '-';
14978
+    }
14979
+    eventProps = el.data(prefix + 'event') || null;
14980
+    if (eventProps) {
14981
+        if (typeof eventProps === 'object') {
14982
+            eventProps = $.extend({}, eventProps); // make a copy
14983
+        }
14984
+        else { // something like 1 or true. still signal event creation
14985
+            eventProps = {};
14986
+        }
14987
+        // pluck special-cased date/time properties
14988
+        startTime = eventProps.start;
14989
+        if (startTime == null) {
14990
+            startTime = eventProps.time;
14991
+        } // accept 'time' as well
14992
+        duration = eventProps.duration;
14993
+        stick = eventProps.stick;
14994
+        delete eventProps.start;
14995
+        delete eventProps.time;
14996
+        delete eventProps.duration;
14997
+        delete eventProps.stick;
14998
+    }
14999
+    // fallback to standalone attribute values for each of the date/time properties
15000
+    if (startTime == null) {
15001
+        startTime = el.data(prefix + 'start');
15002
+    }
15003
+    if (startTime == null) {
15004
+        startTime = el.data(prefix + 'time');
15005
+    } // accept 'time' as well
15006
+    if (duration == null) {
15007
+        duration = el.data(prefix + 'duration');
15008
+    }
15009
+    if (stick == null) {
15010
+        stick = el.data(prefix + 'stick');
15011
+    }
15012
+    // massage into correct data types
15013
+    startTime = startTime != null ? moment.duration(startTime) : null;
15014
+    duration = duration != null ? moment.duration(duration) : null;
15015
+    stick = Boolean(stick);
15016
+    return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
15017
+}
15018
 
15019
 
15020
 /***/ }),
15021
-/* 229 */
15022
+/* 234 */
15023
 /***/ (function(module, exports, __webpack_require__) {
15024
 
15025
 Object.defineProperty(exports, "__esModule", { value: true });
15026
 var tslib_1 = __webpack_require__(2);
15027
-var moment = __webpack_require__(0);
15028
+var $ = __webpack_require__(3);
15029
 var util_1 = __webpack_require__(4);
15030
-var BasicView_1 = __webpack_require__(62);
15031
-var MonthViewDateProfileGenerator_1 = __webpack_require__(253);
15032
-/* A month view with day cells running in rows (one-per-week) and columns
15033
-----------------------------------------------------------------------------------------------------------------------*/
15034
-var MonthView = /** @class */ (function (_super) {
15035
-    tslib_1.__extends(MonthView, _super);
15036
-    function MonthView() {
15037
-        return _super !== null && _super.apply(this, arguments) || this;
15038
+var EventDefMutation_1 = __webpack_require__(39);
15039
+var EventDefDateMutation_1 = __webpack_require__(40);
15040
+var HitDragListener_1 = __webpack_require__(17);
15041
+var Interaction_1 = __webpack_require__(14);
15042
+var EventResizing = /** @class */ (function (_super) {
15043
+    tslib_1.__extends(EventResizing, _super);
15044
+    /*
15045
+    component impements:
15046
+      - bindSegHandlerToEl
15047
+      - publiclyTrigger
15048
+      - diffDates
15049
+      - eventRangesToEventFootprints
15050
+      - isEventInstanceGroupAllowed
15051
+      - getSafeHitFootprint
15052
+    */
15053
+    function EventResizing(component, eventPointing) {
15054
+        var _this = _super.call(this, component) || this;
15055
+        _this.isResizing = false;
15056
+        _this.eventPointing = eventPointing;
15057
+        return _this;
15058
     }
15059
-    // Overrides the default BasicView behavior to have special multi-week auto-height logic
15060
-    MonthView.prototype.setGridHeight = function (height, isAuto) {
15061
-        // if auto, make the height of each row the height that it would be if there were 6 weeks
15062
-        if (isAuto) {
15063
-            height *= this.dayGrid.rowCnt / 6;
15064
+    EventResizing.prototype.end = function () {
15065
+        if (this.dragListener) {
15066
+            this.dragListener.endInteraction();
15067
+        }
15068
+    };
15069
+    EventResizing.prototype.bindToEl = function (el) {
15070
+        var component = this.component;
15071
+        component.bindSegHandlerToEl(el, 'mousedown', this.handleMouseDown.bind(this));
15072
+        component.bindSegHandlerToEl(el, 'touchstart', this.handleTouchStart.bind(this));
15073
+    };
15074
+    EventResizing.prototype.handleMouseDown = function (seg, ev) {
15075
+        if (this.component.canStartResize(seg, ev)) {
15076
+            this.buildDragListener(seg, $(ev.target).is('.fc-start-resizer'))
15077
+                .startInteraction(ev, { distance: 5 });
15078
+        }
15079
+    };
15080
+    EventResizing.prototype.handleTouchStart = function (seg, ev) {
15081
+        if (this.component.canStartResize(seg, ev)) {
15082
+            this.buildDragListener(seg, $(ev.target).is('.fc-start-resizer'))
15083
+                .startInteraction(ev);
15084
+        }
15085
+    };
15086
+    // Creates a listener that tracks the user as they resize an event segment.
15087
+    // Generic enough to work with any type of Grid.
15088
+    EventResizing.prototype.buildDragListener = function (seg, isStart) {
15089
+        var _this = this;
15090
+        var component = this.component;
15091
+        var view = this.view;
15092
+        var calendar = view.calendar;
15093
+        var eventManager = calendar.eventManager;
15094
+        var el = seg.el;
15095
+        var eventDef = seg.footprint.eventDef;
15096
+        var eventInstance = seg.footprint.eventInstance;
15097
+        var isDragging;
15098
+        var resizeMutation; // zoned event date properties. falsy if invalid resize
15099
+        // Tracks mouse movement over the *grid's* coordinate map
15100
+        var dragListener = this.dragListener = new HitDragListener_1.default(component, {
15101
+            scroll: this.opt('dragScroll'),
15102
+            subjectEl: el,
15103
+            interactionStart: function () {
15104
+                isDragging = false;
15105
+            },
15106
+            dragStart: function (ev) {
15107
+                isDragging = true;
15108
+                // ensure a mouseout on the manipulated event has been reported
15109
+                _this.eventPointing.handleMouseout(seg, ev);
15110
+                _this.segResizeStart(seg, ev);
15111
+            },
15112
+            hitOver: function (hit, isOrig, origHit) {
15113
+                var isAllowed = true;
15114
+                var origHitFootprint = component.getSafeHitFootprint(origHit);
15115
+                var hitFootprint = component.getSafeHitFootprint(hit);
15116
+                var mutatedEventInstanceGroup;
15117
+                if (origHitFootprint && hitFootprint) {
15118
+                    resizeMutation = isStart ?
15119
+                        _this.computeEventStartResizeMutation(origHitFootprint, hitFootprint, seg.footprint) :
15120
+                        _this.computeEventEndResizeMutation(origHitFootprint, hitFootprint, seg.footprint);
15121
+                    if (resizeMutation) {
15122
+                        mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(eventDef.id, resizeMutation);
15123
+                        isAllowed = component.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
15124
+                    }
15125
+                    else {
15126
+                        isAllowed = false;
15127
+                    }
15128
+                }
15129
+                else {
15130
+                    isAllowed = false;
15131
+                }
15132
+                if (!isAllowed) {
15133
+                    resizeMutation = null;
15134
+                    util_1.disableCursor();
15135
+                }
15136
+                else if (resizeMutation.isEmpty()) {
15137
+                    // no change. (FYI, event dates might have zones)
15138
+                    resizeMutation = null;
15139
+                }
15140
+                if (resizeMutation) {
15141
+                    view.hideEventsWithId(seg.footprint.eventDef.id);
15142
+                    view.renderEventResize(component.eventRangesToEventFootprints(mutatedEventInstanceGroup.sliceRenderRanges(component.dateProfile.renderUnzonedRange, calendar)), seg);
15143
+                }
15144
+            },
15145
+            hitOut: function () {
15146
+                resizeMutation = null;
15147
+            },
15148
+            hitDone: function () {
15149
+                view.unrenderEventResize(seg);
15150
+                view.showEventsWithId(seg.footprint.eventDef.id);
15151
+                util_1.enableCursor();
15152
+            },
15153
+            interactionEnd: function (ev) {
15154
+                if (isDragging) {
15155
+                    _this.segResizeStop(seg, ev);
15156
+                }
15157
+                if (resizeMutation) { // valid date to resize to?
15158
+                    // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
15159
+                    view.reportEventResize(eventInstance, resizeMutation, el, ev);
15160
+                }
15161
+                _this.dragListener = null;
15162
+            }
15163
+        });
15164
+        return dragListener;
15165
+    };
15166
+    // Called before event segment resizing starts
15167
+    EventResizing.prototype.segResizeStart = function (seg, ev) {
15168
+        this.isResizing = true;
15169
+        this.component.publiclyTrigger('eventResizeStart', {
15170
+            context: seg.el[0],
15171
+            args: [
15172
+                seg.footprint.getEventLegacy(),
15173
+                ev,
15174
+                {},
15175
+                this.view
15176
+            ]
15177
+        });
15178
+    };
15179
+    // Called after event segment resizing stops
15180
+    EventResizing.prototype.segResizeStop = function (seg, ev) {
15181
+        this.isResizing = false;
15182
+        this.component.publiclyTrigger('eventResizeStop', {
15183
+            context: seg.el[0],
15184
+            args: [
15185
+                seg.footprint.getEventLegacy(),
15186
+                ev,
15187
+                {},
15188
+                this.view
15189
+            ]
15190
+        });
15191
+    };
15192
+    // Returns new date-information for an event segment being resized from its start
15193
+    EventResizing.prototype.computeEventStartResizeMutation = function (startFootprint, endFootprint, origEventFootprint) {
15194
+        var origRange = origEventFootprint.componentFootprint.unzonedRange;
15195
+        var startDelta = this.component.diffDates(endFootprint.unzonedRange.getStart(), startFootprint.unzonedRange.getStart());
15196
+        var dateMutation;
15197
+        var eventDefMutation;
15198
+        if (origRange.getStart().add(startDelta) < origRange.getEnd()) {
15199
+            dateMutation = new EventDefDateMutation_1.default();
15200
+            dateMutation.setStartDelta(startDelta);
15201
+            eventDefMutation = new EventDefMutation_1.default();
15202
+            eventDefMutation.setDateMutation(dateMutation);
15203
+            return eventDefMutation;
15204
+        }
15205
+        return false;
15206
+    };
15207
+    // Returns new date-information for an event segment being resized from its end
15208
+    EventResizing.prototype.computeEventEndResizeMutation = function (startFootprint, endFootprint, origEventFootprint) {
15209
+        var origRange = origEventFootprint.componentFootprint.unzonedRange;
15210
+        var endDelta = this.component.diffDates(endFootprint.unzonedRange.getEnd(), startFootprint.unzonedRange.getEnd());
15211
+        var dateMutation;
15212
+        var eventDefMutation;
15213
+        if (origRange.getEnd().add(endDelta) > origRange.getStart()) {
15214
+            dateMutation = new EventDefDateMutation_1.default();
15215
+            dateMutation.setEndDelta(endDelta);
15216
+            eventDefMutation = new EventDefMutation_1.default();
15217
+            eventDefMutation.setDateMutation(dateMutation);
15218
+            return eventDefMutation;
15219
         }
15220
-        util_1.distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
15221
-    };
15222
-    MonthView.prototype.isDateInOtherMonth = function (date, dateProfile) {
15223
-        return date.month() !== moment.utc(dateProfile.currentUnzonedRange.startMs).month(); // TODO: optimize
15224
+        return false;
15225
     };
15226
-    return MonthView;
15227
-}(BasicView_1.default));
15228
-exports.default = MonthView;
15229
-MonthView.prototype.dateProfileGeneratorClass = MonthViewDateProfileGenerator_1.default;
15230
+    return EventResizing;
15231
+}(Interaction_1.default));
15232
+exports.default = EventResizing;
15233
 
15234
 
15235
 /***/ }),
15236
-/* 230 */
15237
+/* 235 */
15238
 /***/ (function(module, exports, __webpack_require__) {
15239
 
15240
 Object.defineProperty(exports, "__esModule", { value: true });
15241
 var tslib_1 = __webpack_require__(2);
15242
-var $ = __webpack_require__(3);
15243
 var util_1 = __webpack_require__(4);
15244
-var UnzonedRange_1 = __webpack_require__(5);
15245
-var View_1 = __webpack_require__(41);
15246
-var Scroller_1 = __webpack_require__(39);
15247
-var ListEventRenderer_1 = __webpack_require__(254);
15248
-var ListEventPointing_1 = __webpack_require__(255);
15249
-/*
15250
-Responsible for the scroller, and forwarding event-related actions into the "grid".
15251
-*/
15252
-var ListView = /** @class */ (function (_super) {
15253
-    tslib_1.__extends(ListView, _super);
15254
-    function ListView(calendar, viewSpec) {
15255
-        var _this = _super.call(this, calendar, viewSpec) || this;
15256
-        _this.segSelector = '.fc-list-item'; // which elements accept event actions
15257
-        _this.scroller = new Scroller_1.default({
15258
-            overflowX: 'hidden',
15259
-            overflowY: 'auto'
15260
-        });
15261
+var EventDefMutation_1 = __webpack_require__(39);
15262
+var EventDefDateMutation_1 = __webpack_require__(40);
15263
+var DragListener_1 = __webpack_require__(59);
15264
+var HitDragListener_1 = __webpack_require__(17);
15265
+var MouseFollower_1 = __webpack_require__(226);
15266
+var Interaction_1 = __webpack_require__(14);
15267
+var EventDragging = /** @class */ (function (_super) {
15268
+    tslib_1.__extends(EventDragging, _super);
15269
+    /*
15270
+    component implements:
15271
+      - bindSegHandlerToEl
15272
+      - publiclyTrigger
15273
+      - diffDates
15274
+      - eventRangesToEventFootprints
15275
+      - isEventInstanceGroupAllowed
15276
+    */
15277
+    function EventDragging(component, eventPointing) {
15278
+        var _this = _super.call(this, component) || this;
15279
+        _this.isDragging = false;
15280
+        _this.eventPointing = eventPointing;
15281
         return _this;
15282
     }
15283
-    ListView.prototype.renderSkeleton = function () {
15284
-        this.el.addClass('fc-list-view ' +
15285
-            this.calendar.theme.getClass('listView'));
15286
-        this.scroller.render();
15287
-        this.scroller.el.appendTo(this.el);
15288
-        this.contentEl = this.scroller.scrollEl; // shortcut
15289
-    };
15290
-    ListView.prototype.unrenderSkeleton = function () {
15291
-        this.scroller.destroy(); // will remove the Grid too
15292
+    EventDragging.prototype.end = function () {
15293
+        if (this.dragListener) {
15294
+            this.dragListener.endInteraction();
15295
+        }
15296
     };
15297
-    ListView.prototype.updateSize = function (totalHeight, isAuto, isResize) {
15298
-        _super.prototype.updateSize.call(this, totalHeight, isAuto, isResize);
15299
-        this.scroller.clear(); // sets height to 'auto' and clears overflow
15300
-        if (!isAuto) {
15301
-            this.scroller.setHeight(this.computeScrollerHeight(totalHeight));
15302
+    EventDragging.prototype.getSelectionDelay = function () {
15303
+        var delay = this.opt('eventLongPressDelay');
15304
+        if (delay == null) {
15305
+            delay = this.opt('longPressDelay'); // fallback
15306
         }
15307
+        return delay;
15308
     };
15309
-    ListView.prototype.computeScrollerHeight = function (totalHeight) {
15310
-        return totalHeight -
15311
-            util_1.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
15312
+    EventDragging.prototype.bindToEl = function (el) {
15313
+        var component = this.component;
15314
+        component.bindSegHandlerToEl(el, 'mousedown', this.handleMousedown.bind(this));
15315
+        component.bindSegHandlerToEl(el, 'touchstart', this.handleTouchStart.bind(this));
15316
     };
15317
-    ListView.prototype.renderDates = function (dateProfile) {
15318
-        var calendar = this.calendar;
15319
-        var dayStart = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.startMs, true);
15320
-        var viewEnd = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.endMs, true);
15321
-        var dayDates = [];
15322
-        var dayRanges = [];
15323
-        while (dayStart < viewEnd) {
15324
-            dayDates.push(dayStart.clone());
15325
-            dayRanges.push(new UnzonedRange_1.default(dayStart, dayStart.clone().add(1, 'day')));
15326
-            dayStart.add(1, 'day');
15327
+    EventDragging.prototype.handleMousedown = function (seg, ev) {
15328
+        if (!this.component.shouldIgnoreMouse() &&
15329
+            this.component.canStartDrag(seg, ev)) {
15330
+            this.buildDragListener(seg).startInteraction(ev, { distance: 5 });
15331
         }
15332
-        this.dayDates = dayDates;
15333
-        this.dayRanges = dayRanges;
15334
-        // all real rendering happens in EventRenderer
15335
     };
15336
-    // slices by day
15337
-    ListView.prototype.componentFootprintToSegs = function (footprint) {
15338
-        var dayRanges = this.dayRanges;
15339
-        var dayIndex;
15340
-        var segRange;
15341
-        var seg;
15342
-        var segs = [];
15343
-        for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex++) {
15344
-            segRange = footprint.unzonedRange.intersect(dayRanges[dayIndex]);
15345
-            if (segRange) {
15346
-                seg = {
15347
-                    startMs: segRange.startMs,
15348
-                    endMs: segRange.endMs,
15349
-                    isStart: segRange.isStart,
15350
-                    isEnd: segRange.isEnd,
15351
-                    dayIndex: dayIndex
15352
-                };
15353
-                segs.push(seg);
15354
-                // detect when footprint won't go fully into the next day,
15355
-                // and mutate the latest seg to the be the end.
15356
-                if (!seg.isEnd && !footprint.isAllDay &&
15357
-                    dayIndex + 1 < dayRanges.length &&
15358
-                    footprint.unzonedRange.endMs < dayRanges[dayIndex + 1].startMs + this.nextDayThreshold) {
15359
-                    seg.endMs = footprint.unzonedRange.endMs;
15360
-                    seg.isEnd = true;
15361
-                    break;
15362
-                }
15363
-            }
15364
+    EventDragging.prototype.handleTouchStart = function (seg, ev) {
15365
+        var component = this.component;
15366
+        var settings = {
15367
+            delay: this.view.isEventDefSelected(seg.footprint.eventDef) ? // already selected?
15368
+                0 : this.getSelectionDelay()
15369
+        };
15370
+        if (component.canStartDrag(seg, ev)) {
15371
+            this.buildDragListener(seg).startInteraction(ev, settings);
15372
+        }
15373
+        else if (component.canStartSelection(seg, ev)) {
15374
+            this.buildSelectListener(seg).startInteraction(ev, settings);
15375
         }
15376
-        return segs;
15377
-    };
15378
-    ListView.prototype.renderEmptyMessage = function () {
15379
-        this.contentEl.html('<div class="fc-list-empty-wrap2">' + // TODO: try less wraps
15380
-            '<div class="fc-list-empty-wrap1">' +
15381
-            '<div class="fc-list-empty">' +
15382
-            util_1.htmlEscape(this.opt('noEventsMessage')) +
15383
-            '</div>' +
15384
-            '</div>' +
15385
-            '</div>');
15386
     };
15387
-    // render the event segments in the view
15388
-    ListView.prototype.renderSegList = function (allSegs) {
15389
-        var segsByDay = this.groupSegsByDay(allSegs); // sparse array
15390
-        var dayIndex;
15391
-        var daySegs;
15392
-        var i;
15393
-        var tableEl = $('<table class="fc-list-table ' + this.calendar.theme.getClass('tableList') + '"><tbody/></table>');
15394
-        var tbodyEl = tableEl.find('tbody');
15395
-        for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) {
15396
-            daySegs = segsByDay[dayIndex];
15397
-            if (daySegs) {
15398
-                // append a day header
15399
-                tbodyEl.append(this.dayHeaderHtml(this.dayDates[dayIndex]));
15400
-                this.eventRenderer.sortEventSegs(daySegs);
15401
-                for (i = 0; i < daySegs.length; i++) {
15402
-                    tbodyEl.append(daySegs[i].el); // append event row
15403
+    // seg isn't draggable, but let's use a generic DragListener
15404
+    // simply for the delay, so it can be selected.
15405
+    // Has side effect of setting/unsetting `dragListener`
15406
+    EventDragging.prototype.buildSelectListener = function (seg) {
15407
+        var _this = this;
15408
+        var view = this.view;
15409
+        var eventDef = seg.footprint.eventDef;
15410
+        var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
15411
+        if (this.dragListener) {
15412
+            return this.dragListener;
15413
+        }
15414
+        var dragListener = this.dragListener = new DragListener_1.default({
15415
+            dragStart: function (ev) {
15416
+                if (dragListener.isTouch &&
15417
+                    !view.isEventDefSelected(eventDef) &&
15418
+                    eventInstance) {
15419
+                    // if not previously selected, will fire after a delay. then, select the event
15420
+                    view.selectEventInstance(eventInstance);
15421
                 }
15422
+            },
15423
+            interactionEnd: function (ev) {
15424
+                _this.dragListener = null;
15425
             }
15426
-        }
15427
-        this.contentEl.empty().append(tableEl);
15428
+        });
15429
+        return dragListener;
15430
     };
15431
-    // Returns a sparse array of arrays, segs grouped by their dayIndex
15432
-    ListView.prototype.groupSegsByDay = function (segs) {
15433
-        var segsByDay = []; // sparse array
15434
-        var i;
15435
-        var seg;
15436
-        for (i = 0; i < segs.length; i++) {
15437
-            seg = segs[i];
15438
-            (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = []))
15439
-                .push(seg);
15440
+    // Builds a listener that will track user-dragging on an event segment.
15441
+    // Generic enough to work with any type of Grid.
15442
+    // Has side effect of setting/unsetting `dragListener`
15443
+    EventDragging.prototype.buildDragListener = function (seg) {
15444
+        var _this = this;
15445
+        var component = this.component;
15446
+        var view = this.view;
15447
+        var calendar = view.calendar;
15448
+        var eventManager = calendar.eventManager;
15449
+        var el = seg.el;
15450
+        var eventDef = seg.footprint.eventDef;
15451
+        var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
15452
+        var isDragging;
15453
+        var mouseFollower; // A clone of the original element that will move with the mouse
15454
+        var eventDefMutation;
15455
+        if (this.dragListener) {
15456
+            return this.dragListener;
15457
         }
15458
-        return segsByDay;
15459
-    };
15460
-    // generates the HTML for the day headers that live amongst the event rows
15461
-    ListView.prototype.dayHeaderHtml = function (dayDate) {
15462
-        var mainFormat = this.opt('listDayFormat');
15463
-        var altFormat = this.opt('listDayAltFormat');
15464
-        return '<tr class="fc-list-heading" data-date="' + dayDate.format('YYYY-MM-DD') + '">' +
15465
-            '<td class="' + (this.calendar.theme.getClass('tableListHeading') ||
15466
-            this.calendar.theme.getClass('widgetHeader')) + '" colspan="3">' +
15467
-            (mainFormat ?
15468
-                this.buildGotoAnchorHtml(dayDate, { 'class': 'fc-list-heading-main' }, util_1.htmlEscape(dayDate.format(mainFormat)) // inner HTML
15469
-                ) :
15470
-                '') +
15471
-            (altFormat ?
15472
-                this.buildGotoAnchorHtml(dayDate, { 'class': 'fc-list-heading-alt' }, util_1.htmlEscape(dayDate.format(altFormat)) // inner HTML
15473
-                ) :
15474
-                '') +
15475
-            '</td>' +
15476
-            '</tr>';
15477
-    };
15478
-    return ListView;
15479
-}(View_1.default));
15480
-exports.default = ListView;
15481
-ListView.prototype.eventRendererClass = ListEventRenderer_1.default;
15482
-ListView.prototype.eventPointingClass = ListEventPointing_1.default;
15483
-
15484
-
15485
-/***/ }),
15486
-/* 231 */,
15487
-/* 232 */,
15488
-/* 233 */,
15489
-/* 234 */,
15490
-/* 235 */,
15491
-/* 236 */
15492
-/***/ (function(module, exports, __webpack_require__) {
15493
-
15494
-var $ = __webpack_require__(3);
15495
-var exportHooks = __webpack_require__(16);
15496
-var util_1 = __webpack_require__(4);
15497
-var Calendar_1 = __webpack_require__(220);
15498
-// for intentional side-effects
15499
-__webpack_require__(10);
15500
-__webpack_require__(47);
15501
-__webpack_require__(256);
15502
-__webpack_require__(257);
15503
-__webpack_require__(260);
15504
-__webpack_require__(261);
15505
-__webpack_require__(262);
15506
-__webpack_require__(263);
15507
-$.fullCalendar = exportHooks;
15508
-$.fn.fullCalendar = function (options) {
15509
-    var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
15510
-    var res = this; // what this function will return (this jQuery object by default)
15511
-    this.each(function (i, _element) {
15512
-        var element = $(_element);
15513
-        var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
15514
-        var singleRes; // the returned value of this single method call
15515
-        // a method call
15516
-        if (typeof options === 'string') {
15517
-            if (options === 'getCalendar') {
15518
-                if (!i) {
15519
-                    res = calendar;
15520
+        // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
15521
+        // of the view.
15522
+        var dragListener = this.dragListener = new HitDragListener_1.default(view, {
15523
+            scroll: this.opt('dragScroll'),
15524
+            subjectEl: el,
15525
+            subjectCenter: true,
15526
+            interactionStart: function (ev) {
15527
+                seg.component = component; // for renderDrag
15528
+                isDragging = false;
15529
+                mouseFollower = new MouseFollower_1.default(seg.el, {
15530
+                    additionalClass: 'fc-dragging',
15531
+                    parentEl: view.el,
15532
+                    opacity: dragListener.isTouch ? null : _this.opt('dragOpacity'),
15533
+                    revertDuration: _this.opt('dragRevertDuration'),
15534
+                    zIndex: 2 // one above the .fc-view
15535
+                });
15536
+                mouseFollower.hide(); // don't show until we know this is a real drag
15537
+                mouseFollower.start(ev);
15538
+            },
15539
+            dragStart: function (ev) {
15540
+                if (dragListener.isTouch &&
15541
+                    !view.isEventDefSelected(eventDef) &&
15542
+                    eventInstance) {
15543
+                    // if not previously selected, will fire after a delay. then, select the event
15544
+                    view.selectEventInstance(eventInstance);
15545
                 }
15546
-            }
15547
-            else if (options === 'destroy') {
15548
-                if (calendar) {
15549
-                    calendar.destroy();
15550
-                    element.removeData('fullCalendar');
15551
+                isDragging = true;
15552
+                // ensure a mouseout on the manipulated event has been reported
15553
+                _this.eventPointing.handleMouseout(seg, ev);
15554
+                _this.segDragStart(seg, ev);
15555
+                view.hideEventsWithId(seg.footprint.eventDef.id);
15556
+            },
15557
+            hitOver: function (hit, isOrig, origHit) {
15558
+                var isAllowed = true;
15559
+                var origFootprint;
15560
+                var footprint;
15561
+                var mutatedEventInstanceGroup;
15562
+                // starting hit could be forced (DayGrid.limit)
15563
+                if (seg.hit) {
15564
+                    origHit = seg.hit;
15565
                 }
15566
-            }
15567
-            else if (!calendar) {
15568
-                util_1.warn('Attempting to call a FullCalendar method on an element with no calendar.');
15569
-            }
15570
-            else if ($.isFunction(calendar[options])) {
15571
-                singleRes = calendar[options].apply(calendar, args);
15572
-                if (!i) {
15573
-                    res = singleRes; // record the first method call result
15574
+                // hit might not belong to this grid, so query origin grid
15575
+                origFootprint = origHit.component.getSafeHitFootprint(origHit);
15576
+                footprint = hit.component.getSafeHitFootprint(hit);
15577
+                if (origFootprint && footprint) {
15578
+                    eventDefMutation = _this.computeEventDropMutation(origFootprint, footprint, eventDef);
15579
+                    if (eventDefMutation) {
15580
+                        mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(eventDef.id, eventDefMutation);
15581
+                        isAllowed = component.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
15582
+                    }
15583
+                    else {
15584
+                        isAllowed = false;
15585
+                    }
15586
                 }
15587
-                if (options === 'destroy') {
15588
-                    element.removeData('fullCalendar');
15589
+                else {
15590
+                    isAllowed = false;
15591
                 }
15592
+                if (!isAllowed) {
15593
+                    eventDefMutation = null;
15594
+                    util_1.disableCursor();
15595
+                }
15596
+                // if a valid drop location, have the subclass render a visual indication
15597
+                if (eventDefMutation &&
15598
+                    view.renderDrag(// truthy if rendered something
15599
+                    component.eventRangesToEventFootprints(mutatedEventInstanceGroup.sliceRenderRanges(component.dateProfile.renderUnzonedRange, calendar)), seg, dragListener.isTouch)) {
15600
+                    mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
15601
+                }
15602
+                else {
15603
+                    mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
15604
+                }
15605
+                if (isOrig) {
15606
+                    // needs to have moved hits to be a valid drop
15607
+                    eventDefMutation = null;
15608
+                }
15609
+            },
15610
+            hitOut: function () {
15611
+                view.unrenderDrag(seg); // unrender whatever was done in renderDrag
15612
+                mouseFollower.show(); // show in case we are moving out of all hits
15613
+                eventDefMutation = null;
15614
+            },
15615
+            hitDone: function () {
15616
+                util_1.enableCursor();
15617
+            },
15618
+            interactionEnd: function (ev) {
15619
+                delete seg.component; // prevent side effects
15620
+                // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
15621
+                mouseFollower.stop(!eventDefMutation, function () {
15622
+                    if (isDragging) {
15623
+                        view.unrenderDrag(seg);
15624
+                        _this.segDragStop(seg, ev);
15625
+                    }
15626
+                    view.showEventsWithId(seg.footprint.eventDef.id);
15627
+                    if (eventDefMutation) {
15628
+                        // no need to re-show original, will rerender all anyways. esp important if eventRenderWait
15629
+                        view.reportEventDrop(eventInstance, eventDefMutation, el, ev);
15630
+                    }
15631
+                });
15632
+                _this.dragListener = null;
15633
             }
15634
-            else {
15635
-                util_1.warn("'" + options + "' is an unknown FullCalendar method.");
15636
-            }
15637
-        }
15638
-        else if (!calendar) {
15639
-            calendar = new Calendar_1.default(element, options);
15640
-            element.data('fullCalendar', calendar);
15641
-            calendar.render();
15642
-        }
15643
-    });
15644
-    return res;
15645
-};
15646
-module.exports = exportHooks;
15647
-
15648
-
15649
-/***/ }),
15650
-/* 237 */
15651
-/***/ (function(module, exports, __webpack_require__) {
15652
-
15653
-Object.defineProperty(exports, "__esModule", { value: true });
15654
-var tslib_1 = __webpack_require__(2);
15655
-var Model_1 = __webpack_require__(48);
15656
-var Component = /** @class */ (function (_super) {
15657
-    tslib_1.__extends(Component, _super);
15658
-    function Component() {
15659
-        return _super !== null && _super.apply(this, arguments) || this;
15660
-    }
15661
-    Component.prototype.setElement = function (el) {
15662
-        this.el = el;
15663
-        this.bindGlobalHandlers();
15664
-        this.renderSkeleton();
15665
-        this.set('isInDom', true);
15666
-    };
15667
-    Component.prototype.removeElement = function () {
15668
-        this.unset('isInDom');
15669
-        this.unrenderSkeleton();
15670
-        this.unbindGlobalHandlers();
15671
-        this.el.remove();
15672
-        // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
15673
-        // We don't null-out the View's other jQuery element references upon destroy,
15674
-        //  so we shouldn't kill this.el either.
15675
-    };
15676
-    Component.prototype.bindGlobalHandlers = function () {
15677
-        // subclasses can override
15678
+        });
15679
+        return dragListener;
15680
     };
15681
-    Component.prototype.unbindGlobalHandlers = function () {
15682
-        // subclasses can override
15683
+    // Called before event segment dragging starts
15684
+    EventDragging.prototype.segDragStart = function (seg, ev) {
15685
+        this.isDragging = true;
15686
+        this.component.publiclyTrigger('eventDragStart', {
15687
+            context: seg.el[0],
15688
+            args: [
15689
+                seg.footprint.getEventLegacy(),
15690
+                ev,
15691
+                {},
15692
+                this.view
15693
+            ]
15694
+        });
15695
     };
15696
-    /*
15697
-    NOTE: Can't have a `render` method. Read the deprecation notice in View::executeDateRender
15698
-    */
15699
-    // Renders the basic structure of the view before any content is rendered
15700
-    Component.prototype.renderSkeleton = function () {
15701
-        // subclasses should implement
15702
+    // Called after event segment dragging stops
15703
+    EventDragging.prototype.segDragStop = function (seg, ev) {
15704
+        this.isDragging = false;
15705
+        this.component.publiclyTrigger('eventDragStop', {
15706
+            context: seg.el[0],
15707
+            args: [
15708
+                seg.footprint.getEventLegacy(),
15709
+                ev,
15710
+                {},
15711
+                this.view
15712
+            ]
15713
+        });
15714
     };
15715
-    // Unrenders the basic structure of the view
15716
-    Component.prototype.unrenderSkeleton = function () {
15717
-        // subclasses should implement
15718
+    // DOES NOT consider overlap/constraint
15719
+    EventDragging.prototype.computeEventDropMutation = function (startFootprint, endFootprint, eventDef) {
15720
+        var eventDefMutation = new EventDefMutation_1.default();
15721
+        eventDefMutation.setDateMutation(this.computeEventDateMutation(startFootprint, endFootprint));
15722
+        return eventDefMutation;
15723
     };
15724
-    return Component;
15725
-}(Model_1.default));
15726
-exports.default = Component;
15727
-
15728
-
15729
-/***/ }),
15730
-/* 238 */
15731
-/***/ (function(module, exports) {
15732
-
15733
-Object.defineProperty(exports, "__esModule", { value: true });
15734
-var Iterator = /** @class */ (function () {
15735
-    function Iterator(items) {
15736
-        this.items = items || [];
15737
-    }
15738
-    /* Calls a method on every item passing the arguments through */
15739
-    Iterator.prototype.proxyCall = function (methodName) {
15740
-        var args = [];
15741
-        for (var _i = 1; _i < arguments.length; _i++) {
15742
-            args[_i - 1] = arguments[_i];
15743
+    EventDragging.prototype.computeEventDateMutation = function (startFootprint, endFootprint) {
15744
+        var date0 = startFootprint.unzonedRange.getStart();
15745
+        var date1 = endFootprint.unzonedRange.getStart();
15746
+        var clearEnd = false;
15747
+        var forceTimed = false;
15748
+        var forceAllDay = false;
15749
+        var dateDelta;
15750
+        var dateMutation;
15751
+        if (startFootprint.isAllDay !== endFootprint.isAllDay) {
15752
+            clearEnd = true;
15753
+            if (endFootprint.isAllDay) {
15754
+                forceAllDay = true;
15755
+                date0.stripTime();
15756
+            }
15757
+            else {
15758
+                forceTimed = true;
15759
+            }
15760
         }
15761
-        var results = [];
15762
-        this.items.forEach(function (item) {
15763
-            results.push(item[methodName].apply(item, args));
15764
-        });
15765
-        return results;
15766
+        dateDelta = this.component.diffDates(date1, date0);
15767
+        dateMutation = new EventDefDateMutation_1.default();
15768
+        dateMutation.clearEnd = clearEnd;
15769
+        dateMutation.forceTimed = forceTimed;
15770
+        dateMutation.forceAllDay = forceAllDay;
15771
+        dateMutation.setDateDelta(dateDelta);
15772
+        return dateMutation;
15773
     };
15774
-    return Iterator;
15775
-}());
15776
-exports.default = Iterator;
15777
+    return EventDragging;
15778
+}(Interaction_1.default));
15779
+exports.default = EventDragging;
15780
 
15781
 
15782
 /***/ }),
15783
-/* 239 */
15784
+/* 236 */
15785
 /***/ (function(module, exports, __webpack_require__) {
15786
 
15787
 Object.defineProperty(exports, "__esModule", { value: true });
15788
-var $ = __webpack_require__(3);
15789
-var util_1 = __webpack_require__(4);
15790
-/* Toolbar with buttons and title
15791
-----------------------------------------------------------------------------------------------------------------------*/
15792
-var Toolbar = /** @class */ (function () {
15793
-    function Toolbar(calendar, toolbarOptions) {
15794
-        this.el = null; // mirrors local `el`
15795
-        this.viewsWithButtons = [];
15796
-        this.calendar = calendar;
15797
-        this.toolbarOptions = toolbarOptions;
15798
+var tslib_1 = __webpack_require__(2);
15799
+var util_1 = __webpack_require__(4);
15800
+var HitDragListener_1 = __webpack_require__(17);
15801
+var ComponentFootprint_1 = __webpack_require__(12);
15802
+var UnzonedRange_1 = __webpack_require__(5);
15803
+var Interaction_1 = __webpack_require__(14);
15804
+var DateSelecting = /** @class */ (function (_super) {
15805
+    tslib_1.__extends(DateSelecting, _super);
15806
+    /*
15807
+    component must implement:
15808
+      - bindDateHandlerToEl
15809
+      - getSafeHitFootprint
15810
+      - renderHighlight
15811
+      - unrenderHighlight
15812
+    */
15813
+    function DateSelecting(component) {
15814
+        var _this = _super.call(this, component) || this;
15815
+        _this.dragListener = _this.buildDragListener();
15816
+        return _this;
15817
     }
15818
-    // method to update toolbar-specific options, not calendar-wide options
15819
-    Toolbar.prototype.setToolbarOptions = function (newToolbarOptions) {
15820
-        this.toolbarOptions = newToolbarOptions;
15821
+    DateSelecting.prototype.end = function () {
15822
+        this.dragListener.endInteraction();
15823
     };
15824
-    // can be called repeatedly and will rerender
15825
-    Toolbar.prototype.render = function () {
15826
-        var sections = this.toolbarOptions.layout;
15827
-        var el = this.el;
15828
-        if (sections) {
15829
-            if (!el) {
15830
-                el = this.el = $("<div class='fc-toolbar " + this.toolbarOptions.extraClasses + "'/>");
15831
-            }
15832
-            else {
15833
-                el.empty();
15834
-            }
15835
-            el.append(this.renderSection('left'))
15836
-                .append(this.renderSection('right'))
15837
-                .append(this.renderSection('center'))
15838
-                .append('<div class="fc-clear"/>');
15839
-        }
15840
-        else {
15841
-            this.removeElement();
15842
+    DateSelecting.prototype.getDelay = function () {
15843
+        var delay = this.opt('selectLongPressDelay');
15844
+        if (delay == null) {
15845
+            delay = this.opt('longPressDelay'); // fallback
15846
         }
15847
+        return delay;
15848
     };
15849
-    Toolbar.prototype.removeElement = function () {
15850
-        if (this.el) {
15851
-            this.el.remove();
15852
-            this.el = null;
15853
-        }
15854
+    DateSelecting.prototype.bindToEl = function (el) {
15855
+        var _this = this;
15856
+        var component = this.component;
15857
+        var dragListener = this.dragListener;
15858
+        component.bindDateHandlerToEl(el, 'mousedown', function (ev) {
15859
+            if (_this.opt('selectable') && !component.shouldIgnoreMouse()) {
15860
+                dragListener.startInteraction(ev, {
15861
+                    distance: _this.opt('selectMinDistance')
15862
+                });
15863
+            }
15864
+        });
15865
+        component.bindDateHandlerToEl(el, 'touchstart', function (ev) {
15866
+            if (_this.opt('selectable') && !component.shouldIgnoreTouch()) {
15867
+                dragListener.startInteraction(ev, {
15868
+                    delay: _this.getDelay()
15869
+                });
15870
+            }
15871
+        });
15872
+        util_1.preventSelection(el);
15873
     };
15874
-    Toolbar.prototype.renderSection = function (position) {
15875
+    // Creates a listener that tracks the user's drag across day elements, for day selecting.
15876
+    DateSelecting.prototype.buildDragListener = function () {
15877
         var _this = this;
15878
-        var calendar = this.calendar;
15879
-        var theme = calendar.theme;
15880
-        var optionsManager = calendar.optionsManager;
15881
-        var viewSpecManager = calendar.viewSpecManager;
15882
-        var sectionEl = $('<div class="fc-' + position + '"/>');
15883
-        var buttonStr = this.toolbarOptions.layout[position];
15884
-        var calendarCustomButtons = optionsManager.get('customButtons') || {};
15885
-        var calendarButtonTextOverrides = optionsManager.overrides.buttonText || {};
15886
-        var calendarButtonText = optionsManager.get('buttonText') || {};
15887
-        if (buttonStr) {
15888
-            $.each(buttonStr.split(' '), function (i, buttonGroupStr) {
15889
-                var groupChildren = $();
15890
-                var isOnlyButtons = true;
15891
-                var groupEl;
15892
-                $.each(buttonGroupStr.split(','), function (j, buttonName) {
15893
-                    var customButtonProps;
15894
-                    var viewSpec;
15895
-                    var buttonClick;
15896
-                    var buttonIcon; // only one of these will be set
15897
-                    var buttonText; // "
15898
-                    var buttonInnerHtml;
15899
-                    var buttonClasses;
15900
-                    var buttonEl;
15901
-                    var buttonAriaAttr;
15902
-                    if (buttonName === 'title') {
15903
-                        groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
15904
-                        isOnlyButtons = false;
15905
+        var component = this.component;
15906
+        var selectionFootprint; // null if invalid selection
15907
+        var dragListener = new HitDragListener_1.default(component, {
15908
+            scroll: this.opt('dragScroll'),
15909
+            interactionStart: function () {
15910
+                selectionFootprint = null;
15911
+            },
15912
+            dragStart: function (ev) {
15913
+                _this.view.unselect(ev); // since we could be rendering a new selection, we want to clear any old one
15914
+            },
15915
+            hitOver: function (hit, isOrig, origHit) {
15916
+                var origHitFootprint;
15917
+                var hitFootprint;
15918
+                if (origHit) { // click needs to have started on a hit
15919
+                    origHitFootprint = component.getSafeHitFootprint(origHit);
15920
+                    hitFootprint = component.getSafeHitFootprint(hit);
15921
+                    if (origHitFootprint && hitFootprint) {
15922
+                        selectionFootprint = _this.computeSelection(origHitFootprint, hitFootprint);
15923
                     }
15924
                     else {
15925
-                        if ((customButtonProps = calendarCustomButtons[buttonName])) {
15926
-                            buttonClick = function (ev) {
15927
-                                if (customButtonProps.click) {
15928
-                                    customButtonProps.click.call(buttonEl[0], ev);
15929
-                                }
15930
-                            };
15931
-                            (buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) ||
15932
-                                (buttonIcon = theme.getIconClass(buttonName)) ||
15933
-                                (buttonText = customButtonProps.text);
15934
-                        }
15935
-                        else if ((viewSpec = viewSpecManager.getViewSpec(buttonName))) {
15936
-                            _this.viewsWithButtons.push(buttonName);
15937
-                            buttonClick = function () {
15938
-                                calendar.changeView(buttonName);
15939
-                            };
15940
-                            (buttonText = viewSpec.buttonTextOverride) ||
15941
-                                (buttonIcon = theme.getIconClass(buttonName)) ||
15942
-                                (buttonText = viewSpec.buttonTextDefault);
15943
-                        }
15944
-                        else if (calendar[buttonName]) {
15945
-                            buttonClick = function () {
15946
-                                calendar[buttonName]();
15947
-                            };
15948
-                            (buttonText = calendarButtonTextOverrides[buttonName]) ||
15949
-                                (buttonIcon = theme.getIconClass(buttonName)) ||
15950
-                                (buttonText = calendarButtonText[buttonName]);
15951
-                            //            ^ everything else is considered default
15952
-                        }
15953
-                        if (buttonClick) {
15954
-                            buttonClasses = [
15955
-                                'fc-' + buttonName + '-button',
15956
-                                theme.getClass('button'),
15957
-                                theme.getClass('stateDefault')
15958
-                            ];
15959
-                            if (buttonText) {
15960
-                                buttonInnerHtml = util_1.htmlEscape(buttonText);
15961
-                                buttonAriaAttr = '';
15962
-                            }
15963
-                            else if (buttonIcon) {
15964
-                                buttonInnerHtml = "<span class='" + buttonIcon + "'></span>";
15965
-                                buttonAriaAttr = ' aria-label="' + buttonName + '"';
15966
-                            }
15967
-                            buttonEl = $(// type="button" so that it doesn't submit a form
15968
-                            '<button type="button" class="' + buttonClasses.join(' ') + '"' +
15969
-                                buttonAriaAttr +
15970
-                                '>' + buttonInnerHtml + '</button>')
15971
-                                .click(function (ev) {
15972
-                                // don't process clicks for disabled buttons
15973
-                                if (!buttonEl.hasClass(theme.getClass('stateDisabled'))) {
15974
-                                    buttonClick(ev);
15975
-                                    // after the click action, if the button becomes the "active" tab, or disabled,
15976
-                                    // it should never have a hover class, so remove it now.
15977
-                                    if (buttonEl.hasClass(theme.getClass('stateActive')) ||
15978
-                                        buttonEl.hasClass(theme.getClass('stateDisabled'))) {
15979
-                                        buttonEl.removeClass(theme.getClass('stateHover'));
15980
-                                    }
15981
-                                }
15982
-                            })
15983
-                                .mousedown(function () {
15984
-                                // the *down* effect (mouse pressed in).
15985
-                                // only on buttons that are not the "active" tab, or disabled
15986
-                                buttonEl
15987
-                                    .not('.' + theme.getClass('stateActive'))
15988
-                                    .not('.' + theme.getClass('stateDisabled'))
15989
-                                    .addClass(theme.getClass('stateDown'));
15990
-                            })
15991
-                                .mouseup(function () {
15992
-                                // undo the *down* effect
15993
-                                buttonEl.removeClass(theme.getClass('stateDown'));
15994
-                            })
15995
-                                .hover(function () {
15996
-                                // the *hover* effect.
15997
-                                // only on buttons that are not the "active" tab, or disabled
15998
-                                buttonEl
15999
-                                    .not('.' + theme.getClass('stateActive'))
16000
-                                    .not('.' + theme.getClass('stateDisabled'))
16001
-                                    .addClass(theme.getClass('stateHover'));
16002
-                            }, function () {
16003
-                                // undo the *hover* effect
16004
-                                buttonEl
16005
-                                    .removeClass(theme.getClass('stateHover'))
16006
-                                    .removeClass(theme.getClass('stateDown')); // if mouseleave happens before mouseup
16007
-                            });
16008
-                            groupChildren = groupChildren.add(buttonEl);
16009
-                        }
16010
+                        selectionFootprint = null;
16011
                     }
16012
-                });
16013
-                if (isOnlyButtons) {
16014
-                    groupChildren
16015
-                        .first().addClass(theme.getClass('cornerLeft')).end()
16016
-                        .last().addClass(theme.getClass('cornerRight')).end();
16017
-                }
16018
-                if (groupChildren.length > 1) {
16019
-                    groupEl = $('<div/>');
16020
-                    if (isOnlyButtons) {
16021
-                        groupEl.addClass(theme.getClass('buttonGroup'));
16022
+                    if (selectionFootprint) {
16023
+                        component.renderSelectionFootprint(selectionFootprint);
16024
+                    }
16025
+                    else if (selectionFootprint === false) {
16026
+                        util_1.disableCursor();
16027
                     }
16028
-                    groupEl.append(groupChildren);
16029
-                    sectionEl.append(groupEl);
16030
                 }
16031
-                else {
16032
-                    sectionEl.append(groupChildren); // 1 or 0 children
16033
+            },
16034
+            hitOut: function () {
16035
+                selectionFootprint = null;
16036
+                component.unrenderSelection();
16037
+            },
16038
+            hitDone: function () {
16039
+                util_1.enableCursor();
16040
+            },
16041
+            interactionEnd: function (ev, isCancelled) {
16042
+                if (!isCancelled && selectionFootprint) {
16043
+                    // the selection will already have been rendered. just report it
16044
+                    _this.view.reportSelection(selectionFootprint, ev);
16045
                 }
16046
-            });
16047
-        }
16048
-        return sectionEl;
16049
+            }
16050
+        });
16051
+        return dragListener;
16052
     };
16053
-    Toolbar.prototype.updateTitle = function (text) {
16054
-        if (this.el) {
16055
-            this.el.find('h2').text(text);
16056
+    // Given the first and last date-spans of a selection, returns another date-span object.
16057
+    // Subclasses can override and provide additional data in the span object. Will be passed to renderSelectionFootprint().
16058
+    // Will return false if the selection is invalid and this should be indicated to the user.
16059
+    // Will return null/undefined if a selection invalid but no error should be reported.
16060
+    DateSelecting.prototype.computeSelection = function (footprint0, footprint1) {
16061
+        var wholeFootprint = this.computeSelectionFootprint(footprint0, footprint1);
16062
+        if (wholeFootprint && !this.isSelectionFootprintAllowed(wholeFootprint)) {
16063
+            return false;
16064
         }
16065
+        return wholeFootprint;
16066
     };
16067
-    Toolbar.prototype.activateButton = function (buttonName) {
16068
-        if (this.el) {
16069
-            this.el.find('.fc-' + buttonName + '-button')
16070
-                .addClass(this.calendar.theme.getClass('stateActive'));
16071
-        }
16072
+    // Given two spans, must return the combination of the two.
16073
+    // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
16074
+    // Assumes both footprints are non-open-ended.
16075
+    DateSelecting.prototype.computeSelectionFootprint = function (footprint0, footprint1) {
16076
+        var ms = [
16077
+            footprint0.unzonedRange.startMs,
16078
+            footprint0.unzonedRange.endMs,
16079
+            footprint1.unzonedRange.startMs,
16080
+            footprint1.unzonedRange.endMs
16081
+        ];
16082
+        ms.sort(util_1.compareNumbers);
16083
+        return new ComponentFootprint_1.default(new UnzonedRange_1.default(ms[0], ms[3]), footprint0.isAllDay);
16084
     };
16085
-    Toolbar.prototype.deactivateButton = function (buttonName) {
16086
-        if (this.el) {
16087
-            this.el.find('.fc-' + buttonName + '-button')
16088
-                .removeClass(this.calendar.theme.getClass('stateActive'));
16089
-        }
16090
+    DateSelecting.prototype.isSelectionFootprintAllowed = function (componentFootprint) {
16091
+        return this.component.dateProfile.validUnzonedRange.containsRange(componentFootprint.unzonedRange) &&
16092
+            this.view.calendar.constraints.isSelectionFootprintAllowed(componentFootprint);
16093
     };
16094
-    Toolbar.prototype.disableButton = function (buttonName) {
16095
-        if (this.el) {
16096
-            this.el.find('.fc-' + buttonName + '-button')
16097
-                .prop('disabled', true)
16098
-                .addClass(this.calendar.theme.getClass('stateDisabled'));
16099
-        }
16100
+    return DateSelecting;
16101
+}(Interaction_1.default));
16102
+exports.default = DateSelecting;
16103
+
16104
+
16105
+/***/ }),
16106
+/* 237 */
16107
+/***/ (function(module, exports, __webpack_require__) {
16108
+
16109
+Object.defineProperty(exports, "__esModule", { value: true });
16110
+var tslib_1 = __webpack_require__(2);
16111
+var HitDragListener_1 = __webpack_require__(17);
16112
+var Interaction_1 = __webpack_require__(14);
16113
+var DateClicking = /** @class */ (function (_super) {
16114
+    tslib_1.__extends(DateClicking, _super);
16115
+    /*
16116
+    component must implement:
16117
+      - bindDateHandlerToEl
16118
+      - getSafeHitFootprint
16119
+      - getHitEl
16120
+    */
16121
+    function DateClicking(component) {
16122
+        var _this = _super.call(this, component) || this;
16123
+        _this.dragListener = _this.buildDragListener();
16124
+        return _this;
16125
+    }
16126
+    DateClicking.prototype.end = function () {
16127
+        this.dragListener.endInteraction();
16128
     };
16129
-    Toolbar.prototype.enableButton = function (buttonName) {
16130
-        if (this.el) {
16131
-            this.el.find('.fc-' + buttonName + '-button')
16132
-                .prop('disabled', false)
16133
-                .removeClass(this.calendar.theme.getClass('stateDisabled'));
16134
-        }
16135
+    DateClicking.prototype.bindToEl = function (el) {
16136
+        var component = this.component;
16137
+        var dragListener = this.dragListener;
16138
+        component.bindDateHandlerToEl(el, 'mousedown', function (ev) {
16139
+            if (!component.shouldIgnoreMouse()) {
16140
+                dragListener.startInteraction(ev);
16141
+            }
16142
+        });
16143
+        component.bindDateHandlerToEl(el, 'touchstart', function (ev) {
16144
+            if (!component.shouldIgnoreTouch()) {
16145
+                dragListener.startInteraction(ev);
16146
+            }
16147
+        });
16148
     };
16149
-    Toolbar.prototype.getViewsWithButtons = function () {
16150
-        return this.viewsWithButtons;
16151
+    // Creates a listener that tracks the user's drag across day elements, for day clicking.
16152
+    DateClicking.prototype.buildDragListener = function () {
16153
+        var _this = this;
16154
+        var component = this.component;
16155
+        var dayClickHit; // null if invalid dayClick
16156
+        var dragListener = new HitDragListener_1.default(component, {
16157
+            scroll: this.opt('dragScroll'),
16158
+            interactionStart: function () {
16159
+                dayClickHit = dragListener.origHit;
16160
+            },
16161
+            hitOver: function (hit, isOrig, origHit) {
16162
+                // if user dragged to another cell at any point, it can no longer be a dayClick
16163
+                if (!isOrig) {
16164
+                    dayClickHit = null;
16165
+                }
16166
+            },
16167
+            hitOut: function () {
16168
+                dayClickHit = null;
16169
+            },
16170
+            interactionEnd: function (ev, isCancelled) {
16171
+                var componentFootprint;
16172
+                if (!isCancelled && dayClickHit) {
16173
+                    componentFootprint = component.getSafeHitFootprint(dayClickHit);
16174
+                    if (componentFootprint) {
16175
+                        _this.view.triggerDayClick(componentFootprint, component.getHitEl(dayClickHit), ev);
16176
+                    }
16177
+                }
16178
+            }
16179
+        });
16180
+        // because dragListener won't be called with any time delay, "dragging" will begin immediately,
16181
+        // which will kill any touchmoving/scrolling. Prevent this.
16182
+        dragListener.shouldCancelTouchScroll = false;
16183
+        dragListener.scrollAlwaysKills = true;
16184
+        return dragListener;
16185
     };
16186
-    return Toolbar;
16187
-}());
16188
-exports.default = Toolbar;
16189
+    return DateClicking;
16190
+}(Interaction_1.default));
16191
+exports.default = DateClicking;
16192
 
16193
 
16194
 /***/ }),
16195
-/* 240 */
16196
+/* 238 */
16197
 /***/ (function(module, exports, __webpack_require__) {
16198
 
16199
 Object.defineProperty(exports, "__esModule", { value: true });
16200
 var tslib_1 = __webpack_require__(2);
16201
+var moment = __webpack_require__(0);
16202
 var $ = __webpack_require__(3);
16203
 var util_1 = __webpack_require__(4);
16204
-var options_1 = __webpack_require__(32);
16205
-var locale_1 = __webpack_require__(31);
16206
-var Model_1 = __webpack_require__(48);
16207
-var OptionsManager = /** @class */ (function (_super) {
16208
-    tslib_1.__extends(OptionsManager, _super);
16209
-    function OptionsManager(_calendar, overrides) {
16210
-        var _this = _super.call(this) || this;
16211
-        _this._calendar = _calendar;
16212
-        _this.overrides = $.extend({}, overrides); // make a copy
16213
-        _this.dynamicOverrides = {};
16214
-        _this.compute();
16215
+var Scroller_1 = __webpack_require__(41);
16216
+var View_1 = __webpack_require__(43);
16217
+var TimeGrid_1 = __webpack_require__(239);
16218
+var DayGrid_1 = __webpack_require__(66);
16219
+var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
16220
+var agendaTimeGridMethods;
16221
+var agendaDayGridMethods;
16222
+/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
16223
+----------------------------------------------------------------------------------------------------------------------*/
16224
+// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
16225
+// Responsible for managing width/height.
16226
+var AgendaView = /** @class */ (function (_super) {
16227
+    tslib_1.__extends(AgendaView, _super);
16228
+    function AgendaView(calendar, viewSpec) {
16229
+        var _this = _super.call(this, calendar, viewSpec) || this;
16230
+        _this.usesMinMaxTime = true; // indicates that minTime/maxTime affects rendering
16231
+        _this.timeGrid = _this.instantiateTimeGrid();
16232
+        _this.addChild(_this.timeGrid);
16233
+        if (_this.opt('allDaySlot')) { // should we display the "all-day" area?
16234
+            _this.dayGrid = _this.instantiateDayGrid(); // the all-day subcomponent of this view
16235
+            _this.addChild(_this.dayGrid);
16236
+        }
16237
+        _this.scroller = new Scroller_1.default({
16238
+            overflowX: 'hidden',
16239
+            overflowY: 'auto'
16240
+        });
16241
         return _this;
16242
     }
16243
-    OptionsManager.prototype.add = function (newOptionHash) {
16244
-        var optionCnt = 0;
16245
-        var optionName;
16246
-        this.recordOverrides(newOptionHash); // will trigger this model's watchers
16247
-        for (optionName in newOptionHash) {
16248
-            optionCnt++;
16249
+    // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass
16250
+    AgendaView.prototype.instantiateTimeGrid = function () {
16251
+        var timeGrid = new this.timeGridClass(this);
16252
+        util_1.copyOwnProps(agendaTimeGridMethods, timeGrid);
16253
+        return timeGrid;
16254
+    };
16255
+    // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass
16256
+    AgendaView.prototype.instantiateDayGrid = function () {
16257
+        var dayGrid = new this.dayGridClass(this);
16258
+        util_1.copyOwnProps(agendaDayGridMethods, dayGrid);
16259
+        return dayGrid;
16260
+    };
16261
+    /* Rendering
16262
+    ------------------------------------------------------------------------------------------------------------------*/
16263
+    AgendaView.prototype.renderSkeleton = function () {
16264
+        var timeGridWrapEl;
16265
+        var timeGridEl;
16266
+        this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml());
16267
+        this.scroller.render();
16268
+        timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container');
16269
+        timeGridEl = $('<div class="fc-time-grid">').appendTo(timeGridWrapEl);
16270
+        this.el.find('.fc-body > tr > td').append(timeGridWrapEl);
16271
+        this.timeGrid.headContainerEl = this.el.find('.fc-head-container');
16272
+        this.timeGrid.setElement(timeGridEl);
16273
+        if (this.dayGrid) {
16274
+            this.dayGrid.setElement(this.el.find('.fc-day-grid'));
16275
+            // have the day-grid extend it's coordinate area over the <hr> dividing the two grids
16276
+            this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
16277
         }
16278
-        // special-case handling of single option change.
16279
-        // if only one option change, `optionName` will be its name.
16280
-        if (optionCnt === 1) {
16281
-            if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') {
16282
-                this._calendar.updateViewSize(true); // isResize=true
16283
-                return;
16284
+    };
16285
+    AgendaView.prototype.unrenderSkeleton = function () {
16286
+        this.timeGrid.removeElement();
16287
+        if (this.dayGrid) {
16288
+            this.dayGrid.removeElement();
16289
+        }
16290
+        this.scroller.destroy();
16291
+    };
16292
+    // Builds the HTML skeleton for the view.
16293
+    // The day-grid and time-grid components will render inside containers defined by this HTML.
16294
+    AgendaView.prototype.renderSkeletonHtml = function () {
16295
+        var theme = this.calendar.theme;
16296
+        return '' +
16297
+            '<table class="' + theme.getClass('tableGrid') + '">' +
16298
+            (this.opt('columnHeader') ?
16299
+                '<thead class="fc-head">' +
16300
+                    '<tr>' +
16301
+                    '<td class="fc-head-container ' + theme.getClass('widgetHeader') + '">&nbsp;</td>' +
16302
+                    '</tr>' +
16303
+                    '</thead>' :
16304
+                '') +
16305
+            '<tbody class="fc-body">' +
16306
+            '<tr>' +
16307
+            '<td class="' + theme.getClass('widgetContent') + '">' +
16308
+            (this.dayGrid ?
16309
+                '<div class="fc-day-grid"></div>' +
16310
+                    '<hr class="fc-divider ' + theme.getClass('widgetHeader') + '"></hr>' :
16311
+                '') +
16312
+            '</td>' +
16313
+            '</tr>' +
16314
+            '</tbody>' +
16315
+            '</table>';
16316
+    };
16317
+    // Generates an HTML attribute string for setting the width of the axis, if it is known
16318
+    AgendaView.prototype.axisStyleAttr = function () {
16319
+        if (this.axisWidth != null) {
16320
+            return 'style="width:' + this.axisWidth + 'px"';
16321
+        }
16322
+        return '';
16323
+    };
16324
+    /* Now Indicator
16325
+    ------------------------------------------------------------------------------------------------------------------*/
16326
+    AgendaView.prototype.getNowIndicatorUnit = function () {
16327
+        return this.timeGrid.getNowIndicatorUnit();
16328
+    };
16329
+    /* Dimensions
16330
+    ------------------------------------------------------------------------------------------------------------------*/
16331
+    // Adjusts the vertical dimensions of the view to the specified values
16332
+    AgendaView.prototype.updateSize = function (totalHeight, isAuto, isResize) {
16333
+        var eventLimit;
16334
+        var scrollerHeight;
16335
+        var scrollbarWidths;
16336
+        _super.prototype.updateSize.call(this, totalHeight, isAuto, isResize);
16337
+        // make all axis cells line up, and record the width so newly created axis cells will have it
16338
+        this.axisWidth = util_1.matchCellWidths(this.el.find('.fc-axis'));
16339
+        // hack to give the view some height prior to timeGrid's columns being rendered
16340
+        // TODO: separate setting height from scroller VS timeGrid.
16341
+        if (!this.timeGrid.colEls) {
16342
+            if (!isAuto) {
16343
+                scrollerHeight = this.computeScrollerHeight(totalHeight);
16344
+                this.scroller.setHeight(scrollerHeight);
16345
             }
16346
-            else if (optionName === 'defaultDate') {
16347
-                return; // can't change date this way. use gotoDate instead
16348
+            return;
16349
+        }
16350
+        // set of fake row elements that must compensate when scroller has scrollbars
16351
+        var noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)');
16352
+        // reset all dimensions back to the original state
16353
+        this.timeGrid.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
16354
+        this.scroller.clear(); // sets height to 'auto' and clears overflow
16355
+        util_1.uncompensateScroll(noScrollRowEls);
16356
+        // limit number of events in the all-day area
16357
+        if (this.dayGrid) {
16358
+            this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
16359
+            eventLimit = this.opt('eventLimit');
16360
+            if (eventLimit && typeof eventLimit !== 'number') {
16361
+                eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number
16362
             }
16363
-            else if (optionName === 'businessHours') {
16364
-                return; // this model already reacts to this
16365
+            if (eventLimit) {
16366
+                this.dayGrid.limitRows(eventLimit);
16367
             }
16368
-            else if (/^(event|select)(Overlap|Constraint|Allow)$/.test(optionName)) {
16369
-                return; // doesn't affect rendering. only interactions.
16370
+        }
16371
+        if (!isAuto) { // should we force dimensions of the scroll container?
16372
+            scrollerHeight = this.computeScrollerHeight(totalHeight);
16373
+            this.scroller.setHeight(scrollerHeight);
16374
+            scrollbarWidths = this.scroller.getScrollbarWidths();
16375
+            if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
16376
+                // make the all-day and header rows lines up
16377
+                util_1.compensateScroll(noScrollRowEls, scrollbarWidths);
16378
+                // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
16379
+                // and reapply the desired height to the scroller.
16380
+                scrollerHeight = this.computeScrollerHeight(totalHeight);
16381
+                this.scroller.setHeight(scrollerHeight);
16382
             }
16383
-            else if (optionName === 'timezone') {
16384
-                this._calendar.view.flash('initialEvents');
16385
-                return;
16386
+            // guarantees the same scrollbar widths
16387
+            this.scroller.lockOverflow(scrollbarWidths);
16388
+            // if there's any space below the slats, show the horizontal rule.
16389
+            // this won't cause any new overflow, because lockOverflow already called.
16390
+            if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) {
16391
+                this.timeGrid.bottomRuleEl.show();
16392
             }
16393
         }
16394
-        // catch-all. rerender the header and footer and rebuild/rerender the current view
16395
-        this._calendar.renderHeader();
16396
-        this._calendar.renderFooter();
16397
-        // even non-current views will be affected by this option change. do before rerender
16398
-        // TODO: detangle
16399
-        this._calendar.viewsByType = {};
16400
-        this._calendar.reinitView();
16401
     };
16402
-    // Computes the flattened options hash for the calendar and assigns to `this.options`.
16403
-    // Assumes this.overrides and this.dynamicOverrides have already been initialized.
16404
-    OptionsManager.prototype.compute = function () {
16405
-        var locale;
16406
-        var localeDefaults;
16407
-        var isRTL;
16408
-        var dirDefaults;
16409
-        var rawOptions;
16410
-        locale = util_1.firstDefined(// explicit locale option given?
16411
-        this.dynamicOverrides.locale, this.overrides.locale);
16412
-        localeDefaults = locale_1.localeOptionHash[locale];
16413
-        if (!localeDefaults) {
16414
-            locale = options_1.globalDefaults.locale;
16415
-            localeDefaults = locale_1.localeOptionHash[locale] || {};
16416
-        }
16417
-        isRTL = util_1.firstDefined(// based on options computed so far, is direction RTL?
16418
-        this.dynamicOverrides.isRTL, this.overrides.isRTL, localeDefaults.isRTL, options_1.globalDefaults.isRTL);
16419
-        dirDefaults = isRTL ? options_1.rtlDefaults : {};
16420
-        this.dirDefaults = dirDefaults;
16421
-        this.localeDefaults = localeDefaults;
16422
-        rawOptions = options_1.mergeOptions([
16423
-            options_1.globalDefaults,
16424
-            dirDefaults,
16425
-            localeDefaults,
16426
-            this.overrides,
16427
-            this.dynamicOverrides
16428
-        ]);
16429
-        locale_1.populateInstanceComputableOptions(rawOptions); // fill in gaps with computed options
16430
-        this.reset(rawOptions);
16431
+    // given a desired total height of the view, returns what the height of the scroller should be
16432
+    AgendaView.prototype.computeScrollerHeight = function (totalHeight) {
16433
+        return totalHeight -
16434
+            util_1.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
16435
     };
16436
-    // stores the new options internally, but does not rerender anything.
16437
-    OptionsManager.prototype.recordOverrides = function (newOptionHash) {
16438
-        var optionName;
16439
-        for (optionName in newOptionHash) {
16440
-            this.dynamicOverrides[optionName] = newOptionHash[optionName];
16441
+    /* Scroll
16442
+    ------------------------------------------------------------------------------------------------------------------*/
16443
+    // Computes the initial pre-configured scroll state prior to allowing the user to change it
16444
+    AgendaView.prototype.computeInitialDateScroll = function () {
16445
+        var scrollTime = moment.duration(this.opt('scrollTime'));
16446
+        var top = this.timeGrid.computeTimeTop(scrollTime);
16447
+        // zoom can give weird floating-point values. rather scroll a little bit further
16448
+        top = Math.ceil(top);
16449
+        if (top) {
16450
+            top++; // to overcome top border that slots beyond the first have. looks better
16451
         }
16452
-        this._calendar.viewSpecManager.clearCache(); // the dynamic override invalidates the options in this cache, so just clear it
16453
-        this.compute(); // this.options needs to be recomputed after the dynamic override
16454
-    };
16455
-    return OptionsManager;
16456
-}(Model_1.default));
16457
-exports.default = OptionsManager;
16458
-
16459
-
16460
-/***/ }),
16461
-/* 241 */
16462
-/***/ (function(module, exports, __webpack_require__) {
16463
-
16464
-Object.defineProperty(exports, "__esModule", { value: true });
16465
-var moment = __webpack_require__(0);
16466
-var $ = __webpack_require__(3);
16467
-var ViewRegistry_1 = __webpack_require__(22);
16468
-var util_1 = __webpack_require__(4);
16469
-var options_1 = __webpack_require__(32);
16470
-var locale_1 = __webpack_require__(31);
16471
-var ViewSpecManager = /** @class */ (function () {
16472
-    function ViewSpecManager(optionsManager, _calendar) {
16473
-        this.optionsManager = optionsManager;
16474
-        this._calendar = _calendar;
16475
-        this.clearCache();
16476
-    }
16477
-    ViewSpecManager.prototype.clearCache = function () {
16478
-        this.viewSpecCache = {};
16479
+        return { top: top };
16480
     };
16481
-    // Gets information about how to create a view. Will use a cache.
16482
-    ViewSpecManager.prototype.getViewSpec = function (viewType) {
16483
-        var cache = this.viewSpecCache;
16484
-        return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
16485
+    AgendaView.prototype.queryDateScroll = function () {
16486
+        return { top: this.scroller.getScrollTop() };
16487
     };
16488
-    // Given a duration singular unit, like "week" or "day", finds a matching view spec.
16489
-    // Preference is given to views that have corresponding buttons.
16490
-    ViewSpecManager.prototype.getUnitViewSpec = function (unit) {
16491
-        var viewTypes;
16492
-        var i;
16493
-        var spec;
16494
-        if ($.inArray(unit, util_1.unitsDesc) !== -1) {
16495
-            // put views that have buttons first. there will be duplicates, but oh well
16496
-            viewTypes = this._calendar.header.getViewsWithButtons(); // TODO: include footer as well?
16497
-            $.each(ViewRegistry_1.viewHash, function (viewType) {
16498
-                viewTypes.push(viewType);
16499
-            });
16500
-            for (i = 0; i < viewTypes.length; i++) {
16501
-                spec = this.getViewSpec(viewTypes[i]);
16502
-                if (spec) {
16503
-                    if (spec.singleUnit === unit) {
16504
-                        return spec;
16505
-                    }
16506
-                }
16507
-            }
16508
+    AgendaView.prototype.applyDateScroll = function (scroll) {
16509
+        if (scroll.top !== undefined) {
16510
+            this.scroller.setScrollTop(scroll.top);
16511
         }
16512
     };
16513
-    // Builds an object with information on how to create a given view
16514
-    ViewSpecManager.prototype.buildViewSpec = function (requestedViewType) {
16515
-        var viewOverrides = this.optionsManager.overrides.views || {};
16516
-        var specChain = []; // for the view. lowest to highest priority
16517
-        var defaultsChain = []; // for the view. lowest to highest priority
16518
-        var overridesChain = []; // for the view. lowest to highest priority
16519
-        var viewType = requestedViewType;
16520
-        var spec; // for the view
16521
-        var overrides; // for the view
16522
-        var durationInput;
16523
-        var duration;
16524
-        var unit;
16525
-        // iterate from the specific view definition to a more general one until we hit an actual View class
16526
-        while (viewType) {
16527
-            spec = ViewRegistry_1.viewHash[viewType];
16528
-            overrides = viewOverrides[viewType];
16529
-            viewType = null; // clear. might repopulate for another iteration
16530
-            if (typeof spec === 'function') {
16531
-                spec = { 'class': spec };
16532
-            }
16533
-            if (spec) {
16534
-                specChain.unshift(spec);
16535
-                defaultsChain.unshift(spec.defaults || {});
16536
-                durationInput = durationInput || spec.duration;
16537
-                viewType = viewType || spec.type;
16538
+    /* Hit Areas
16539
+    ------------------------------------------------------------------------------------------------------------------*/
16540
+    // forward all hit-related method calls to the grids (dayGrid might not be defined)
16541
+    AgendaView.prototype.getHitFootprint = function (hit) {
16542
+        // TODO: hit.component is set as a hack to identify where the hit came from
16543
+        return hit.component.getHitFootprint(hit);
16544
+    };
16545
+    AgendaView.prototype.getHitEl = function (hit) {
16546
+        // TODO: hit.component is set as a hack to identify where the hit came from
16547
+        return hit.component.getHitEl(hit);
16548
+    };
16549
+    /* Event Rendering
16550
+    ------------------------------------------------------------------------------------------------------------------*/
16551
+    AgendaView.prototype.executeEventRender = function (eventsPayload) {
16552
+        var dayEventsPayload = {};
16553
+        var timedEventsPayload = {};
16554
+        var id;
16555
+        var eventInstanceGroup;
16556
+        // separate the events into all-day and timed
16557
+        for (id in eventsPayload) {
16558
+            eventInstanceGroup = eventsPayload[id];
16559
+            if (eventInstanceGroup.getEventDef().isAllDay()) {
16560
+                dayEventsPayload[id] = eventInstanceGroup;
16561
             }
16562
-            if (overrides) {
16563
-                overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
16564
-                durationInput = durationInput || overrides.duration;
16565
-                viewType = viewType || overrides.type;
16566
+            else {
16567
+                timedEventsPayload[id] = eventInstanceGroup;
16568
             }
16569
         }
16570
-        spec = util_1.mergeProps(specChain);
16571
-        spec.type = requestedViewType;
16572
-        if (!spec['class']) {
16573
-            return false;
16574
+        this.timeGrid.executeEventRender(timedEventsPayload);
16575
+        if (this.dayGrid) {
16576
+            this.dayGrid.executeEventRender(dayEventsPayload);
16577
         }
16578
-        // fall back to top-level `duration` option
16579
-        durationInput = durationInput ||
16580
-            this.optionsManager.dynamicOverrides.duration ||
16581
-            this.optionsManager.overrides.duration;
16582
-        if (durationInput) {
16583
-            duration = moment.duration(durationInput);
16584
-            if (duration.valueOf()) {
16585
-                unit = util_1.computeDurationGreatestUnit(duration, durationInput);
16586
-                spec.duration = duration;
16587
-                spec.durationUnit = unit;
16588
-                // view is a single-unit duration, like "week" or "day"
16589
-                // incorporate options for this. lowest priority
16590
-                if (duration.as(unit) === 1) {
16591
-                    spec.singleUnit = unit;
16592
-                    overridesChain.unshift(viewOverrides[unit] || {});
16593
-                }
16594
-            }
16595
+    };
16596
+    /* Dragging/Resizing Routing
16597
+    ------------------------------------------------------------------------------------------------------------------*/
16598
+    // A returned value of `true` signals that a mock "helper" event has been rendered.
16599
+    AgendaView.prototype.renderDrag = function (eventFootprints, seg, isTouch) {
16600
+        var groups = groupEventFootprintsByAllDay(eventFootprints);
16601
+        var renderedHelper = false;
16602
+        renderedHelper = this.timeGrid.renderDrag(groups.timed, seg, isTouch);
16603
+        if (this.dayGrid) {
16604
+            renderedHelper = this.dayGrid.renderDrag(groups.allDay, seg, isTouch) || renderedHelper;
16605
         }
16606
-        spec.defaults = options_1.mergeOptions(defaultsChain);
16607
-        spec.overrides = options_1.mergeOptions(overridesChain);
16608
-        this.buildViewSpecOptions(spec);
16609
-        this.buildViewSpecButtonText(spec, requestedViewType);
16610
-        return spec;
16611
+        return renderedHelper;
16612
     };
16613
-    // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
16614
-    ViewSpecManager.prototype.buildViewSpecOptions = function (spec) {
16615
-        var optionsManager = this.optionsManager;
16616
-        spec.options = options_1.mergeOptions([
16617
-            options_1.globalDefaults,
16618
-            spec.defaults,
16619
-            optionsManager.dirDefaults,
16620
-            optionsManager.localeDefaults,
16621
-            optionsManager.overrides,
16622
-            spec.overrides,
16623
-            optionsManager.dynamicOverrides // dynamically set via setter. highest precedence
16624
-        ]);
16625
-        locale_1.populateInstanceComputableOptions(spec.options);
16626
+    AgendaView.prototype.renderEventResize = function (eventFootprints, seg, isTouch) {
16627
+        var groups = groupEventFootprintsByAllDay(eventFootprints);
16628
+        this.timeGrid.renderEventResize(groups.timed, seg, isTouch);
16629
+        if (this.dayGrid) {
16630
+            this.dayGrid.renderEventResize(groups.allDay, seg, isTouch);
16631
+        }
16632
     };
16633
-    // Computes and assigns a view spec's buttonText-related options
16634
-    ViewSpecManager.prototype.buildViewSpecButtonText = function (spec, requestedViewType) {
16635
-        var optionsManager = this.optionsManager;
16636
-        // given an options object with a possible `buttonText` hash, lookup the buttonText for the
16637
-        // requested view, falling back to a generic unit entry like "week" or "day"
16638
-        function queryButtonText(options) {
16639
-            var buttonText = options.buttonText || {};
16640
-            return buttonText[requestedViewType] ||
16641
-                // view can decide to look up a certain key
16642
-                (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) ||
16643
-                // a key like "month"
16644
-                (spec.singleUnit ? buttonText[spec.singleUnit] : null);
16645
+    /* Selection
16646
+    ------------------------------------------------------------------------------------------------------------------*/
16647
+    // Renders a visual indication of a selection
16648
+    AgendaView.prototype.renderSelectionFootprint = function (componentFootprint) {
16649
+        if (!componentFootprint.isAllDay) {
16650
+            this.timeGrid.renderSelectionFootprint(componentFootprint);
16651
+        }
16652
+        else if (this.dayGrid) {
16653
+            this.dayGrid.renderSelectionFootprint(componentFootprint);
16654
         }
16655
-        // highest to lowest priority
16656
-        spec.buttonTextOverride =
16657
-            queryButtonText(optionsManager.dynamicOverrides) ||
16658
-                queryButtonText(optionsManager.overrides) || // constructor-specified buttonText lookup hash takes precedence
16659
-                spec.overrides.buttonText; // `buttonText` for view-specific options is a string
16660
-        // highest to lowest priority. mirrors buildViewSpecOptions
16661
-        spec.buttonTextDefault =
16662
-            queryButtonText(optionsManager.localeDefaults) ||
16663
-                queryButtonText(optionsManager.dirDefaults) ||
16664
-                spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
16665
-                queryButtonText(options_1.globalDefaults) ||
16666
-                (spec.duration ? this._calendar.humanizeDuration(spec.duration) : null) || // like "3 days"
16667
-                requestedViewType; // fall back to given view name
16668
     };
16669
-    return ViewSpecManager;
16670
-}());
16671
-exports.default = ViewSpecManager;
16672
+    return AgendaView;
16673
+}(View_1.default));
16674
+exports.default = AgendaView;
16675
+AgendaView.prototype.timeGridClass = TimeGrid_1.default;
16676
+AgendaView.prototype.dayGridClass = DayGrid_1.default;
16677
+// Will customize the rendering behavior of the AgendaView's timeGrid
16678
+agendaTimeGridMethods = {
16679
+    // Generates the HTML that will go before the day-of week header cells
16680
+    renderHeadIntroHtml: function () {
16681
+        var view = this.view;
16682
+        var calendar = view.calendar;
16683
+        var weekStart = calendar.msToUtcMoment(this.dateProfile.renderUnzonedRange.startMs, true);
16684
+        var weekText;
16685
+        if (this.opt('weekNumbers')) {
16686
+            weekText = weekStart.format(this.opt('smallWeekFormat'));
16687
+            return '' +
16688
+                '<th class="fc-axis fc-week-number ' + calendar.theme.getClass('widgetHeader') + '" ' + view.axisStyleAttr() + '>' +
16689
+                view.buildGotoAnchorHtml(// aside from link, important for matchCellWidths
16690
+                { date: weekStart, type: 'week', forceOff: this.colCnt > 1 }, util_1.htmlEscape(weekText) // inner HTML
16691
+                ) +
16692
+                '</th>';
16693
+        }
16694
+        else {
16695
+            return '<th class="fc-axis ' + calendar.theme.getClass('widgetHeader') + '" ' + view.axisStyleAttr() + '></th>';
16696
+        }
16697
+    },
16698
+    // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
16699
+    renderBgIntroHtml: function () {
16700
+        var view = this.view;
16701
+        return '<td class="fc-axis ' + view.calendar.theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '></td>';
16702
+    },
16703
+    // Generates the HTML that goes before all other types of cells.
16704
+    // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
16705
+    renderIntroHtml: function () {
16706
+        var view = this.view;
16707
+        return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
16708
+    }
16709
+};
16710
+// Will customize the rendering behavior of the AgendaView's dayGrid
16711
+agendaDayGridMethods = {
16712
+    // Generates the HTML that goes before the all-day cells
16713
+    renderBgIntroHtml: function () {
16714
+        var view = this.view;
16715
+        return '' +
16716
+            '<td class="fc-axis ' + view.calendar.theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '>' +
16717
+            '<span>' + // needed for matchCellWidths
16718
+            view.getAllDayHtml() +
16719
+            '</span>' +
16720
+            '</td>';
16721
+    },
16722
+    // Generates the HTML that goes before all other types of cells.
16723
+    // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
16724
+    renderIntroHtml: function () {
16725
+        var view = this.view;
16726
+        return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>';
16727
+    }
16728
+};
16729
+function groupEventFootprintsByAllDay(eventFootprints) {
16730
+    var allDay = [];
16731
+    var timed = [];
16732
+    var i;
16733
+    for (i = 0; i < eventFootprints.length; i++) {
16734
+        if (eventFootprints[i].componentFootprint.isAllDay) {
16735
+            allDay.push(eventFootprints[i]);
16736
+        }
16737
+        else {
16738
+            timed.push(eventFootprints[i]);
16739
+        }
16740
+    }
16741
+    return { allDay: allDay, timed: timed };
16742
+}
16743
 
16744
 
16745
 /***/ }),
16746
-/* 242 */
16747
+/* 239 */
16748
 /***/ (function(module, exports, __webpack_require__) {
16749
 
16750
 Object.defineProperty(exports, "__esModule", { value: true });
16751
+var tslib_1 = __webpack_require__(2);
16752
 var $ = __webpack_require__(3);
16753
+var moment = __webpack_require__(0);
16754
 var util_1 = __webpack_require__(4);
16755
-var EventPeriod_1 = __webpack_require__(243);
16756
-var ArrayEventSource_1 = __webpack_require__(52);
16757
-var EventSource_1 = __webpack_require__(6);
16758
-var EventSourceParser_1 = __webpack_require__(38);
16759
-var SingleEventDef_1 = __webpack_require__(13);
16760
-var EventInstanceGroup_1 = __webpack_require__(18);
16761
-var EmitterMixin_1 = __webpack_require__(11);
16762
-var ListenerMixin_1 = __webpack_require__(7);
16763
-var EventManager = /** @class */ (function () {
16764
-    function EventManager(calendar) {
16765
-        this.calendar = calendar;
16766
-        this.stickySource = new ArrayEventSource_1.default(calendar);
16767
-        this.otherSources = [];
16768
+var InteractiveDateComponent_1 = __webpack_require__(42);
16769
+var BusinessHourRenderer_1 = __webpack_require__(61);
16770
+var StandardInteractionsMixin_1 = __webpack_require__(65);
16771
+var DayTableMixin_1 = __webpack_require__(60);
16772
+var CoordCache_1 = __webpack_require__(58);
16773
+var UnzonedRange_1 = __webpack_require__(5);
16774
+var ComponentFootprint_1 = __webpack_require__(12);
16775
+var TimeGridEventRenderer_1 = __webpack_require__(240);
16776
+var TimeGridHelperRenderer_1 = __webpack_require__(241);
16777
+var TimeGridFillRenderer_1 = __webpack_require__(242);
16778
+/* A component that renders one or more columns of vertical time slots
16779
+----------------------------------------------------------------------------------------------------------------------*/
16780
+// We mixin DayTable, even though there is only a single row of days
16781
+// potential nice values for the slot-duration and interval-duration
16782
+// from largest to smallest
16783
+var AGENDA_STOCK_SUB_DURATIONS = [
16784
+    { hours: 1 },
16785
+    { minutes: 30 },
16786
+    { minutes: 15 },
16787
+    { seconds: 30 },
16788
+    { seconds: 15 }
16789
+];
16790
+var TimeGrid = /** @class */ (function (_super) {
16791
+    tslib_1.__extends(TimeGrid, _super);
16792
+    function TimeGrid(view) {
16793
+        var _this = _super.call(this, view) || this;
16794
+        _this.processOptions();
16795
+        return _this;
16796
     }
16797
-    EventManager.prototype.requestEvents = function (start, end, timezone, force) {
16798
-        if (force ||
16799
-            !this.currentPeriod ||
16800
-            !this.currentPeriod.isWithinRange(start, end) ||
16801
-            timezone !== this.currentPeriod.timezone) {
16802
-            this.setPeriod(// will change this.currentPeriod
16803
-            new EventPeriod_1.default(start, end, timezone));
16804
-        }
16805
-        return this.currentPeriod.whenReleased();
16806
-    };
16807
-    // Source Adding/Removing
16808
-    // -----------------------------------------------------------------------------------------------------------------
16809
-    EventManager.prototype.addSource = function (eventSource) {
16810
-        this.otherSources.push(eventSource);
16811
-        if (this.currentPeriod) {
16812
-            this.currentPeriod.requestSource(eventSource); // might release
16813
-        }
16814
-    };
16815
-    EventManager.prototype.removeSource = function (doomedSource) {
16816
-        util_1.removeExact(this.otherSources, doomedSource);
16817
-        if (this.currentPeriod) {
16818
-            this.currentPeriod.purgeSource(doomedSource); // might release
16819
-        }
16820
-    };
16821
-    EventManager.prototype.removeAllSources = function () {
16822
-        this.otherSources = [];
16823
-        if (this.currentPeriod) {
16824
-            this.currentPeriod.purgeAllSources(); // might release
16825
-        }
16826
-    };
16827
-    // Source Refetching
16828
-    // -----------------------------------------------------------------------------------------------------------------
16829
-    EventManager.prototype.refetchSource = function (eventSource) {
16830
-        var currentPeriod = this.currentPeriod;
16831
-        if (currentPeriod) {
16832
-            currentPeriod.freeze();
16833
-            currentPeriod.purgeSource(eventSource);
16834
-            currentPeriod.requestSource(eventSource);
16835
-            currentPeriod.thaw();
16836
+    // Slices up the given span (unzoned start/end with other misc data) into an array of segments
16837
+    TimeGrid.prototype.componentFootprintToSegs = function (componentFootprint) {
16838
+        var segs = this.sliceRangeByTimes(componentFootprint.unzonedRange);
16839
+        var i;
16840
+        for (i = 0; i < segs.length; i++) {
16841
+            if (this.isRTL) {
16842
+                segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex;
16843
+            }
16844
+            else {
16845
+                segs[i].col = segs[i].dayIndex;
16846
+            }
16847
         }
16848
+        return segs;
16849
     };
16850
-    EventManager.prototype.refetchAllSources = function () {
16851
-        var currentPeriod = this.currentPeriod;
16852
-        if (currentPeriod) {
16853
-            currentPeriod.freeze();
16854
-            currentPeriod.purgeAllSources();
16855
-            currentPeriod.requestSources(this.getSources());
16856
-            currentPeriod.thaw();
16857
+    /* Date Handling
16858
+    ------------------------------------------------------------------------------------------------------------------*/
16859
+    TimeGrid.prototype.sliceRangeByTimes = function (unzonedRange) {
16860
+        var segs = [];
16861
+        var segRange;
16862
+        var dayIndex;
16863
+        for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) {
16864
+            segRange = unzonedRange.intersect(this.dayRanges[dayIndex]);
16865
+            if (segRange) {
16866
+                segs.push({
16867
+                    startMs: segRange.startMs,
16868
+                    endMs: segRange.endMs,
16869
+                    isStart: segRange.isStart,
16870
+                    isEnd: segRange.isEnd,
16871
+                    dayIndex: dayIndex
16872
+                });
16873
+            }
16874
         }
16875
+        return segs;
16876
     };
16877
-    // Source Querying
16878
-    // -----------------------------------------------------------------------------------------------------------------
16879
-    EventManager.prototype.getSources = function () {
16880
-        return [this.stickySource].concat(this.otherSources);
16881
-    };
16882
-    // like querySources, but accepts multple match criteria (like multiple IDs)
16883
-    EventManager.prototype.multiQuerySources = function (matchInputs) {
16884
-        // coerce into an array
16885
-        if (!matchInputs) {
16886
-            matchInputs = [];
16887
-        }
16888
-        else if (!$.isArray(matchInputs)) {
16889
-            matchInputs = [matchInputs];
16890
-        }
16891
-        var matchingSources = [];
16892
-        var i;
16893
-        // resolve raw inputs to real event source objects
16894
-        for (i = 0; i < matchInputs.length; i++) {
16895
-            matchingSources.push.apply(// append
16896
-            matchingSources, this.querySources(matchInputs[i]));
16897
+    /* Options
16898
+    ------------------------------------------------------------------------------------------------------------------*/
16899
+    // Parses various options into properties of this object
16900
+    TimeGrid.prototype.processOptions = function () {
16901
+        var slotDuration = this.opt('slotDuration');
16902
+        var snapDuration = this.opt('snapDuration');
16903
+        var input;
16904
+        slotDuration = moment.duration(slotDuration);
16905
+        snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
16906
+        this.slotDuration = slotDuration;
16907
+        this.snapDuration = snapDuration;
16908
+        this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple?
16909
+        // might be an array value (for TimelineView).
16910
+        // if so, getting the most granular entry (the last one probably).
16911
+        input = this.opt('slotLabelFormat');
16912
+        if ($.isArray(input)) {
16913
+            input = input[input.length - 1];
16914
         }
16915
-        return matchingSources;
16916
+        this.labelFormat = input ||
16917
+            this.opt('smallTimeFormat'); // the computed default
16918
+        input = this.opt('slotLabelInterval');
16919
+        this.labelInterval = input ?
16920
+            moment.duration(input) :
16921
+            this.computeLabelInterval(slotDuration);
16922
     };
16923
-    // matchInput can either by a real event source object, an ID, or the function/URL for the source.
16924
-    // returns an array of matching source objects.
16925
-    EventManager.prototype.querySources = function (matchInput) {
16926
-        var sources = this.otherSources;
16927
-        var i;
16928
-        var source;
16929
-        // given a proper event source object
16930
-        for (i = 0; i < sources.length; i++) {
16931
-            source = sources[i];
16932
-            if (source === matchInput) {
16933
-                return [source];
16934
+    // Computes an automatic value for slotLabelInterval
16935
+    TimeGrid.prototype.computeLabelInterval = function (slotDuration) {
16936
+        var i;
16937
+        var labelInterval;
16938
+        var slotsPerLabel;
16939
+        // find the smallest stock label interval that results in more than one slots-per-label
16940
+        for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) {
16941
+            labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]);
16942
+            slotsPerLabel = util_1.divideDurationByDuration(labelInterval, slotDuration);
16943
+            if (util_1.isInt(slotsPerLabel) && slotsPerLabel > 1) {
16944
+                return labelInterval;
16945
             }
16946
         }
16947
-        // an ID match
16948
-        source = this.getSourceById(EventSource_1.default.normalizeId(matchInput));
16949
-        if (source) {
16950
-            return [source];
16951
-        }
16952
-        // parse as an event source
16953
-        matchInput = EventSourceParser_1.default.parse(matchInput, this.calendar);
16954
-        if (matchInput) {
16955
-            return $.grep(sources, function (source) {
16956
-                return isSourcesEquivalent(matchInput, source);
16957
-            });
16958
-        }
16959
+        return moment.duration(slotDuration); // fall back. clone
16960
     };
16961
-    /*
16962
-    ID assumed to already be normalized
16963
-    */
16964
-    EventManager.prototype.getSourceById = function (id) {
16965
-        return $.grep(this.otherSources, function (source) {
16966
-            return source.id && source.id === id;
16967
-        })[0];
16968
+    /* Date Rendering
16969
+    ------------------------------------------------------------------------------------------------------------------*/
16970
+    TimeGrid.prototype.renderDates = function (dateProfile) {
16971
+        this.dateProfile = dateProfile;
16972
+        this.updateDayTable();
16973
+        this.renderSlats();
16974
+        this.renderColumns();
16975
     };
16976
-    // Event-Period
16977
-    // -----------------------------------------------------------------------------------------------------------------
16978
-    EventManager.prototype.setPeriod = function (eventPeriod) {
16979
-        if (this.currentPeriod) {
16980
-            this.unbindPeriod(this.currentPeriod);
16981
-            this.currentPeriod = null;
16982
-        }
16983
-        this.currentPeriod = eventPeriod;
16984
-        this.bindPeriod(eventPeriod);
16985
-        eventPeriod.requestSources(this.getSources());
16986
+    TimeGrid.prototype.unrenderDates = function () {
16987
+        // this.unrenderSlats(); // don't need this because repeated .html() calls clear
16988
+        this.unrenderColumns();
16989
     };
16990
-    EventManager.prototype.bindPeriod = function (eventPeriod) {
16991
-        this.listenTo(eventPeriod, 'release', function (eventsPayload) {
16992
-            this.trigger('release', eventsPayload);
16993
-        });
16994
+    TimeGrid.prototype.renderSkeleton = function () {
16995
+        var theme = this.view.calendar.theme;
16996
+        this.el.html('<div class="fc-bg"></div>' +
16997
+            '<div class="fc-slats"></div>' +
16998
+            '<hr class="fc-divider ' + theme.getClass('widgetHeader') + '" style="display:none"></hr>');
16999
+        this.bottomRuleEl = this.el.find('hr');
17000
     };
17001
-    EventManager.prototype.unbindPeriod = function (eventPeriod) {
17002
-        this.stopListeningTo(eventPeriod);
17003
+    TimeGrid.prototype.renderSlats = function () {
17004
+        var theme = this.view.calendar.theme;
17005
+        this.slatContainerEl = this.el.find('> .fc-slats')
17006
+            .html(// avoids needing ::unrenderSlats()
17007
+        '<table class="' + theme.getClass('tableGrid') + '">' +
17008
+            this.renderSlatRowHtml() +
17009
+            '</table>');
17010
+        this.slatEls = this.slatContainerEl.find('tr');
17011
+        this.slatCoordCache = new CoordCache_1.default({
17012
+            els: this.slatEls,
17013
+            isVertical: true
17014
+        });
17015
     };
17016
-    // Event Getting/Adding/Removing
17017
-    // -----------------------------------------------------------------------------------------------------------------
17018
-    EventManager.prototype.getEventDefByUid = function (uid) {
17019
-        if (this.currentPeriod) {
17020
-            return this.currentPeriod.getEventDefByUid(uid);
17021
+    // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
17022
+    TimeGrid.prototype.renderSlatRowHtml = function () {
17023
+        var view = this.view;
17024
+        var calendar = view.calendar;
17025
+        var theme = calendar.theme;
17026
+        var isRTL = this.isRTL;
17027
+        var dateProfile = this.dateProfile;
17028
+        var html = '';
17029
+        var slotTime = moment.duration(+dateProfile.minTime); // wish there was .clone() for durations
17030
+        var slotIterator = moment.duration(0);
17031
+        var slotDate; // will be on the view's first day, but we only care about its time
17032
+        var isLabeled;
17033
+        var axisHtml;
17034
+        // Calculate the time for each slot
17035
+        while (slotTime < dateProfile.maxTime) {
17036
+            slotDate = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.startMs).time(slotTime);
17037
+            isLabeled = util_1.isInt(util_1.divideDurationByDuration(slotIterator, this.labelInterval));
17038
+            axisHtml =
17039
+                '<td class="fc-axis fc-time ' + theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '>' +
17040
+                    (isLabeled ?
17041
+                        '<span>' + // for matchCellWidths
17042
+                            util_1.htmlEscape(slotDate.format(this.labelFormat)) +
17043
+                            '</span>' :
17044
+                        '') +
17045
+                    '</td>';
17046
+            html +=
17047
+                '<tr data-time="' + slotDate.format('HH:mm:ss') + '"' +
17048
+                    (isLabeled ? '' : ' class="fc-minor"') +
17049
+                    '>' +
17050
+                    (!isRTL ? axisHtml : '') +
17051
+                    '<td class="' + theme.getClass('widgetContent') + '"></td>' +
17052
+                    (isRTL ? axisHtml : '') +
17053
+                    '</tr>';
17054
+            slotTime.add(this.slotDuration);
17055
+            slotIterator.add(this.slotDuration);
17056
         }
17057
+        return html;
17058
     };
17059
-    EventManager.prototype.addEventDef = function (eventDef, isSticky) {
17060
-        if (isSticky) {
17061
-            this.stickySource.addEventDef(eventDef);
17062
+    TimeGrid.prototype.renderColumns = function () {
17063
+        var dateProfile = this.dateProfile;
17064
+        var theme = this.view.calendar.theme;
17065
+        this.dayRanges = this.dayDates.map(function (dayDate) {
17066
+            return new UnzonedRange_1.default(dayDate.clone().add(dateProfile.minTime), dayDate.clone().add(dateProfile.maxTime));
17067
+        });
17068
+        if (this.headContainerEl) {
17069
+            this.headContainerEl.html(this.renderHeadHtml());
17070
         }
17071
-        if (this.currentPeriod) {
17072
-            this.currentPeriod.addEventDef(eventDef); // might release
17073
+        this.el.find('> .fc-bg').html('<table class="' + theme.getClass('tableGrid') + '">' +
17074
+            this.renderBgTrHtml(0) + // row=0
17075
+            '</table>');
17076
+        this.colEls = this.el.find('.fc-day, .fc-disabled-day');
17077
+        this.colCoordCache = new CoordCache_1.default({
17078
+            els: this.colEls,
17079
+            isHorizontal: true
17080
+        });
17081
+        this.renderContentSkeleton();
17082
+    };
17083
+    TimeGrid.prototype.unrenderColumns = function () {
17084
+        this.unrenderContentSkeleton();
17085
+    };
17086
+    /* Content Skeleton
17087
+    ------------------------------------------------------------------------------------------------------------------*/
17088
+    // Renders the DOM that the view's content will live in
17089
+    TimeGrid.prototype.renderContentSkeleton = function () {
17090
+        var cellHtml = '';
17091
+        var i;
17092
+        var skeletonEl;
17093
+        for (i = 0; i < this.colCnt; i++) {
17094
+            cellHtml +=
17095
+                '<td>' +
17096
+                    '<div class="fc-content-col">' +
17097
+                    '<div class="fc-event-container fc-helper-container"></div>' +
17098
+                    '<div class="fc-event-container"></div>' +
17099
+                    '<div class="fc-highlight-container"></div>' +
17100
+                    '<div class="fc-bgevent-container"></div>' +
17101
+                    '<div class="fc-business-container"></div>' +
17102
+                    '</div>' +
17103
+                    '</td>';
17104
         }
17105
+        skeletonEl = this.contentSkeletonEl = $('<div class="fc-content-skeleton">' +
17106
+            '<table>' +
17107
+            '<tr>' + cellHtml + '</tr>' +
17108
+            '</table>' +
17109
+            '</div>');
17110
+        this.colContainerEls = skeletonEl.find('.fc-content-col');
17111
+        this.helperContainerEls = skeletonEl.find('.fc-helper-container');
17112
+        this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)');
17113
+        this.bgContainerEls = skeletonEl.find('.fc-bgevent-container');
17114
+        this.highlightContainerEls = skeletonEl.find('.fc-highlight-container');
17115
+        this.businessContainerEls = skeletonEl.find('.fc-business-container');
17116
+        this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level
17117
+        this.el.append(skeletonEl);
17118
     };
17119
-    EventManager.prototype.removeEventDefsById = function (eventId) {
17120
-        this.getSources().forEach(function (eventSource) {
17121
-            eventSource.removeEventDefsById(eventId);
17122
-        });
17123
-        if (this.currentPeriod) {
17124
-            this.currentPeriod.removeEventDefsById(eventId); // might release
17125
+    TimeGrid.prototype.unrenderContentSkeleton = function () {
17126
+        if (this.contentSkeletonEl) { // defensive :(
17127
+            this.contentSkeletonEl.remove();
17128
+            this.contentSkeletonEl = null;
17129
+            this.colContainerEls = null;
17130
+            this.helperContainerEls = null;
17131
+            this.fgContainerEls = null;
17132
+            this.bgContainerEls = null;
17133
+            this.highlightContainerEls = null;
17134
+            this.businessContainerEls = null;
17135
         }
17136
     };
17137
-    EventManager.prototype.removeAllEventDefs = function () {
17138
-        this.getSources().forEach(function (eventSource) {
17139
-            eventSource.removeAllEventDefs();
17140
-        });
17141
-        if (this.currentPeriod) {
17142
-            this.currentPeriod.removeAllEventDefs();
17143
+    // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
17144
+    TimeGrid.prototype.groupSegsByCol = function (segs) {
17145
+        var segsByCol = [];
17146
+        var i;
17147
+        for (i = 0; i < this.colCnt; i++) {
17148
+            segsByCol.push([]);
17149
+        }
17150
+        for (i = 0; i < segs.length; i++) {
17151
+            segsByCol[segs[i].col].push(segs[i]);
17152
         }
17153
+        return segsByCol;
17154
     };
17155
-    // Event Mutating
17156
-    // -----------------------------------------------------------------------------------------------------------------
17157
-    /*
17158
-    Returns an undo function.
17159
-    */
17160
-    EventManager.prototype.mutateEventsWithId = function (eventDefId, eventDefMutation) {
17161
-        var currentPeriod = this.currentPeriod;
17162
-        var eventDefs;
17163
-        var undoFuncs = [];
17164
-        if (currentPeriod) {
17165
-            currentPeriod.freeze();
17166
-            eventDefs = currentPeriod.getEventDefsById(eventDefId);
17167
-            eventDefs.forEach(function (eventDef) {
17168
-                // add/remove esp because id might change
17169
-                currentPeriod.removeEventDef(eventDef);
17170
-                undoFuncs.push(eventDefMutation.mutateSingle(eventDef));
17171
-                currentPeriod.addEventDef(eventDef);
17172
-            });
17173
-            currentPeriod.thaw();
17174
-            return function () {
17175
-                currentPeriod.freeze();
17176
-                for (var i = 0; i < eventDefs.length; i++) {
17177
-                    currentPeriod.removeEventDef(eventDefs[i]);
17178
-                    undoFuncs[i]();
17179
-                    currentPeriod.addEventDef(eventDefs[i]);
17180
-                }
17181
-                currentPeriod.thaw();
17182
-            };
17183
+    // Given segments grouped by column, insert the segments' elements into a parallel array of container
17184
+    // elements, each living within a column.
17185
+    TimeGrid.prototype.attachSegsByCol = function (segsByCol, containerEls) {
17186
+        var col;
17187
+        var segs;
17188
+        var i;
17189
+        for (col = 0; col < this.colCnt; col++) { // iterate each column grouping
17190
+            segs = segsByCol[col];
17191
+            for (i = 0; i < segs.length; i++) {
17192
+                containerEls.eq(col).append(segs[i].el);
17193
+            }
17194
         }
17195
-        return function () { };
17196
     };
17197
-    /*
17198
-    copies and then mutates
17199
-    */
17200
-    EventManager.prototype.buildMutatedEventInstanceGroup = function (eventDefId, eventDefMutation) {
17201
-        var eventDefs = this.getEventDefsById(eventDefId);
17202
+    /* Now Indicator
17203
+    ------------------------------------------------------------------------------------------------------------------*/
17204
+    TimeGrid.prototype.getNowIndicatorUnit = function () {
17205
+        return 'minute'; // will refresh on the minute
17206
+    };
17207
+    TimeGrid.prototype.renderNowIndicator = function (date) {
17208
+        // HACK: if date columns not ready for some reason (scheduler)
17209
+        if (!this.colContainerEls) {
17210
+            return;
17211
+        }
17212
+        // seg system might be overkill, but it handles scenario where line needs to be rendered
17213
+        //  more than once because of columns with the same date (resources columns for example)
17214
+        var segs = this.componentFootprintToSegs(new ComponentFootprint_1.default(new UnzonedRange_1.default(date, date.valueOf() + 1), // protect against null range
17215
+        false // all-day
17216
+        ));
17217
+        var top = this.computeDateTop(date, date);
17218
+        var nodes = [];
17219
         var i;
17220
-        var defCopy;
17221
-        var allInstances = [];
17222
-        for (i = 0; i < eventDefs.length; i++) {
17223
-            defCopy = eventDefs[i].clone();
17224
-            if (defCopy instanceof SingleEventDef_1.default) {
17225
-                eventDefMutation.mutateSingle(defCopy);
17226
-                allInstances.push.apply(allInstances, // append
17227
-                defCopy.buildInstances());
17228
-            }
17229
+        // render lines within the columns
17230
+        for (i = 0; i < segs.length; i++) {
17231
+            nodes.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>')
17232
+                .css('top', top)
17233
+                .appendTo(this.colContainerEls.eq(segs[i].col))[0]);
17234
         }
17235
-        return new EventInstanceGroup_1.default(allInstances);
17236
+        // render an arrow over the axis
17237
+        if (segs.length > 0) { // is the current time in view?
17238
+            nodes.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>')
17239
+                .css('top', top)
17240
+                .appendTo(this.el.find('.fc-content-skeleton'))[0]);
17241
+        }
17242
+        this.nowIndicatorEls = $(nodes);
17243
     };
17244
-    // Freezing
17245
-    // -----------------------------------------------------------------------------------------------------------------
17246
-    EventManager.prototype.freeze = function () {
17247
-        if (this.currentPeriod) {
17248
-            this.currentPeriod.freeze();
17249
+    TimeGrid.prototype.unrenderNowIndicator = function () {
17250
+        if (this.nowIndicatorEls) {
17251
+            this.nowIndicatorEls.remove();
17252
+            this.nowIndicatorEls = null;
17253
         }
17254
     };
17255
-    EventManager.prototype.thaw = function () {
17256
-        if (this.currentPeriod) {
17257
-            this.currentPeriod.thaw();
17258
+    /* Coordinates
17259
+    ------------------------------------------------------------------------------------------------------------------*/
17260
+    TimeGrid.prototype.updateSize = function (totalHeight, isAuto, isResize) {
17261
+        _super.prototype.updateSize.call(this, totalHeight, isAuto, isResize);
17262
+        this.slatCoordCache.build();
17263
+        if (isResize) {
17264
+            this.updateSegVerticals([].concat(this.eventRenderer.getSegs(), this.businessSegs || []));
17265
         }
17266
     };
17267
-    // methods that simply forward to EventPeriod
17268
-    EventManager.prototype.getEventDefsById = function (eventDefId) {
17269
-        return this.currentPeriod.getEventDefsById(eventDefId);
17270
+    TimeGrid.prototype.getTotalSlatHeight = function () {
17271
+        return this.slatContainerEl.outerHeight();
17272
     };
17273
-    EventManager.prototype.getEventInstances = function () {
17274
-        return this.currentPeriod.getEventInstances();
17275
+    // Computes the top coordinate, relative to the bounds of the grid, of the given date.
17276
+    // `ms` can be a millisecond UTC time OR a UTC moment.
17277
+    // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
17278
+    TimeGrid.prototype.computeDateTop = function (ms, startOfDayDate) {
17279
+        return this.computeTimeTop(moment.duration(ms - startOfDayDate.clone().stripTime()));
17280
     };
17281
-    EventManager.prototype.getEventInstancesWithId = function (eventDefId) {
17282
-        return this.currentPeriod.getEventInstancesWithId(eventDefId);
17283
+    // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
17284
+    TimeGrid.prototype.computeTimeTop = function (time) {
17285
+        var len = this.slatEls.length;
17286
+        var dateProfile = this.dateProfile;
17287
+        var slatCoverage = (time - dateProfile.minTime) / this.slotDuration; // floating-point value of # of slots covered
17288
+        var slatIndex;
17289
+        var slatRemainder;
17290
+        // compute a floating-point number for how many slats should be progressed through.
17291
+        // from 0 to number of slats (inclusive)
17292
+        // constrained because minTime/maxTime might be customized.
17293
+        slatCoverage = Math.max(0, slatCoverage);
17294
+        slatCoverage = Math.min(len, slatCoverage);
17295
+        // an integer index of the furthest whole slat
17296
+        // from 0 to number slats (*exclusive*, so len-1)
17297
+        slatIndex = Math.floor(slatCoverage);
17298
+        slatIndex = Math.min(slatIndex, len - 1);
17299
+        // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
17300
+        // could be 1.0 if slatCoverage is covering *all* the slots
17301
+        slatRemainder = slatCoverage - slatIndex;
17302
+        return this.slatCoordCache.getTopPosition(slatIndex) +
17303
+            this.slatCoordCache.getHeight(slatIndex) * slatRemainder;
17304
     };
17305
-    EventManager.prototype.getEventInstancesWithoutId = function (eventDefId) {
17306
-        return this.currentPeriod.getEventInstancesWithoutId(eventDefId);
17307
+    // Refreshes the CSS top/bottom coordinates for each segment element.
17308
+    // Works when called after initial render, after a window resize/zoom for example.
17309
+    TimeGrid.prototype.updateSegVerticals = function (segs) {
17310
+        this.computeSegVerticals(segs);
17311
+        this.assignSegVerticals(segs);
17312
     };
17313
-    return EventManager;
17314
-}());
17315
-exports.default = EventManager;
17316
-EmitterMixin_1.default.mixInto(EventManager);
17317
-ListenerMixin_1.default.mixInto(EventManager);
17318
-function isSourcesEquivalent(source0, source1) {
17319
-    return source0.getPrimitive() === source1.getPrimitive();
17320
-}
17321
-
17322
-
17323
-/***/ }),
17324
-/* 243 */
17325
-/***/ (function(module, exports, __webpack_require__) {
17326
-
17327
-Object.defineProperty(exports, "__esModule", { value: true });
17328
-var $ = __webpack_require__(3);
17329
-var util_1 = __webpack_require__(4);
17330
-var Promise_1 = __webpack_require__(20);
17331
-var EmitterMixin_1 = __webpack_require__(11);
17332
-var UnzonedRange_1 = __webpack_require__(5);
17333
-var EventInstanceGroup_1 = __webpack_require__(18);
17334
-var EventPeriod = /** @class */ (function () {
17335
-    function EventPeriod(start, end, timezone) {
17336
-        this.pendingCnt = 0;
17337
-        this.freezeDepth = 0;
17338
-        this.stuntedReleaseCnt = 0;
17339
-        this.releaseCnt = 0;
17340
-        this.start = start;
17341
-        this.end = end;
17342
-        this.timezone = timezone;
17343
-        this.unzonedRange = new UnzonedRange_1.default(start.clone().stripZone(), end.clone().stripZone());
17344
-        this.requestsByUid = {};
17345
-        this.eventDefsByUid = {};
17346
-        this.eventDefsById = {};
17347
-        this.eventInstanceGroupsById = {};
17348
-    }
17349
-    EventPeriod.prototype.isWithinRange = function (start, end) {
17350
-        // TODO: use a range util function?
17351
-        return !start.isBefore(this.start) && !end.isAfter(this.end);
17352
+    // For each segment in an array, computes and assigns its top and bottom properties
17353
+    TimeGrid.prototype.computeSegVerticals = function (segs) {
17354
+        var eventMinHeight = this.opt('agendaEventMinHeight');
17355
+        var i;
17356
+        var seg;
17357
+        var dayDate;
17358
+        for (i = 0; i < segs.length; i++) {
17359
+            seg = segs[i];
17360
+            dayDate = this.dayDates[seg.dayIndex];
17361
+            seg.top = this.computeDateTop(seg.startMs, dayDate);
17362
+            seg.bottom = Math.max(seg.top + eventMinHeight, this.computeDateTop(seg.endMs, dayDate));
17363
+        }
17364
     };
17365
-    // Requesting and Purging
17366
-    // -----------------------------------------------------------------------------------------------------------------
17367
-    EventPeriod.prototype.requestSources = function (sources) {
17368
-        this.freeze();
17369
-        for (var i = 0; i < sources.length; i++) {
17370
-            this.requestSource(sources[i]);
17371
+    // Given segments that already have their top/bottom properties computed, applies those values to
17372
+    // the segments' elements.
17373
+    TimeGrid.prototype.assignSegVerticals = function (segs) {
17374
+        var i;
17375
+        var seg;
17376
+        for (i = 0; i < segs.length; i++) {
17377
+            seg = segs[i];
17378
+            seg.el.css(this.generateSegVerticalCss(seg));
17379
         }
17380
-        this.thaw();
17381
     };
17382
-    EventPeriod.prototype.requestSource = function (source) {
17383
-        var _this = this;
17384
-        var request = { source: source, status: 'pending', eventDefs: null };
17385
-        this.requestsByUid[source.uid] = request;
17386
-        this.pendingCnt += 1;
17387
-        source.fetch(this.start, this.end, this.timezone).then(function (eventDefs) {
17388
-            if (request.status !== 'cancelled') {
17389
-                request.status = 'completed';
17390
-                request.eventDefs = eventDefs;
17391
-                _this.addEventDefs(eventDefs);
17392
-                _this.pendingCnt--;
17393
-                _this.tryRelease();
17394
-            }
17395
-        }, function () {
17396
-            if (request.status !== 'cancelled') {
17397
-                request.status = 'failed';
17398
-                _this.pendingCnt--;
17399
-                _this.tryRelease();
17400
-            }
17401
-        });
17402
+    // Generates an object with CSS properties for the top/bottom coordinates of a segment element
17403
+    TimeGrid.prototype.generateSegVerticalCss = function (seg) {
17404
+        return {
17405
+            top: seg.top,
17406
+            bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
17407
+        };
17408
     };
17409
-    EventPeriod.prototype.purgeSource = function (source) {
17410
-        var request = this.requestsByUid[source.uid];
17411
-        if (request) {
17412
-            delete this.requestsByUid[source.uid];
17413
-            if (request.status === 'pending') {
17414
-                request.status = 'cancelled';
17415
-                this.pendingCnt--;
17416
-                this.tryRelease();
17417
-            }
17418
-            else if (request.status === 'completed') {
17419
-                request.eventDefs.forEach(this.removeEventDef.bind(this));
17420
+    /* Hit System
17421
+    ------------------------------------------------------------------------------------------------------------------*/
17422
+    TimeGrid.prototype.prepareHits = function () {
17423
+        this.colCoordCache.build();
17424
+        this.slatCoordCache.build();
17425
+    };
17426
+    TimeGrid.prototype.releaseHits = function () {
17427
+        this.colCoordCache.clear();
17428
+        // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop
17429
+    };
17430
+    TimeGrid.prototype.queryHit = function (leftOffset, topOffset) {
17431
+        var snapsPerSlot = this.snapsPerSlot;
17432
+        var colCoordCache = this.colCoordCache;
17433
+        var slatCoordCache = this.slatCoordCache;
17434
+        if (colCoordCache.isLeftInBounds(leftOffset) && slatCoordCache.isTopInBounds(topOffset)) {
17435
+            var colIndex = colCoordCache.getHorizontalIndex(leftOffset);
17436
+            var slatIndex = slatCoordCache.getVerticalIndex(topOffset);
17437
+            if (colIndex != null && slatIndex != null) {
17438
+                var slatTop = slatCoordCache.getTopOffset(slatIndex);
17439
+                var slatHeight = slatCoordCache.getHeight(slatIndex);
17440
+                var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1
17441
+                var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat
17442
+                var snapIndex = slatIndex * snapsPerSlot + localSnapIndex;
17443
+                var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight;
17444
+                var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight;
17445
+                return {
17446
+                    col: colIndex,
17447
+                    snap: snapIndex,
17448
+                    component: this,
17449
+                    left: colCoordCache.getLeftOffset(colIndex),
17450
+                    right: colCoordCache.getRightOffset(colIndex),
17451
+                    top: snapTop,
17452
+                    bottom: snapBottom
17453
+                };
17454
             }
17455
         }
17456
     };
17457
-    EventPeriod.prototype.purgeAllSources = function () {
17458
-        var requestsByUid = this.requestsByUid;
17459
-        var uid;
17460
-        var request;
17461
-        var completedCnt = 0;
17462
-        for (uid in requestsByUid) {
17463
-            request = requestsByUid[uid];
17464
-            if (request.status === 'pending') {
17465
-                request.status = 'cancelled';
17466
-            }
17467
-            else if (request.status === 'completed') {
17468
-                completedCnt++;
17469
+    TimeGrid.prototype.getHitFootprint = function (hit) {
17470
+        var start = this.getCellDate(0, hit.col); // row=0
17471
+        var time = this.computeSnapTime(hit.snap); // pass in the snap-index
17472
+        var end;
17473
+        start.time(time);
17474
+        end = start.clone().add(this.snapDuration);
17475
+        return new ComponentFootprint_1.default(new UnzonedRange_1.default(start, end), false // all-day?
17476
+        );
17477
+    };
17478
+    // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
17479
+    TimeGrid.prototype.computeSnapTime = function (snapIndex) {
17480
+        return moment.duration(this.dateProfile.minTime + this.snapDuration * snapIndex);
17481
+    };
17482
+    TimeGrid.prototype.getHitEl = function (hit) {
17483
+        return this.colEls.eq(hit.col);
17484
+    };
17485
+    /* Event Drag Visualization
17486
+    ------------------------------------------------------------------------------------------------------------------*/
17487
+    // Renders a visual indication of an event being dragged over the specified date(s).
17488
+    // A returned value of `true` signals that a mock "helper" event has been rendered.
17489
+    TimeGrid.prototype.renderDrag = function (eventFootprints, seg, isTouch) {
17490
+        var i;
17491
+        if (seg) { // if there is event information for this drag, render a helper event
17492
+            if (eventFootprints.length) {
17493
+                this.helperRenderer.renderEventDraggingFootprints(eventFootprints, seg, isTouch);
17494
+                // signal that a helper has been rendered
17495
+                return true;
17496
             }
17497
         }
17498
-        this.requestsByUid = {};
17499
-        this.pendingCnt = 0;
17500
-        if (completedCnt) {
17501
-            this.removeAllEventDefs(); // might release
17502
+        else { // otherwise, just render a highlight
17503
+            for (i = 0; i < eventFootprints.length; i++) {
17504
+                this.renderHighlight(eventFootprints[i].componentFootprint);
17505
+            }
17506
         }
17507
     };
17508
-    // Event Definitions
17509
-    // -----------------------------------------------------------------------------------------------------------------
17510
-    EventPeriod.prototype.getEventDefByUid = function (eventDefUid) {
17511
-        return this.eventDefsByUid[eventDefUid];
17512
+    // Unrenders any visual indication of an event being dragged
17513
+    TimeGrid.prototype.unrenderDrag = function () {
17514
+        this.unrenderHighlight();
17515
+        this.helperRenderer.unrender();
17516
     };
17517
-    EventPeriod.prototype.getEventDefsById = function (eventDefId) {
17518
-        var a = this.eventDefsById[eventDefId];
17519
-        if (a) {
17520
-            return a.slice(); // clone
17521
-        }
17522
-        return [];
17523
+    /* Event Resize Visualization
17524
+    ------------------------------------------------------------------------------------------------------------------*/
17525
+    // Renders a visual indication of an event being resized
17526
+    TimeGrid.prototype.renderEventResize = function (eventFootprints, seg, isTouch) {
17527
+        this.helperRenderer.renderEventResizingFootprints(eventFootprints, seg, isTouch);
17528
     };
17529
-    EventPeriod.prototype.addEventDefs = function (eventDefs) {
17530
-        for (var i = 0; i < eventDefs.length; i++) {
17531
-            this.addEventDef(eventDefs[i]);
17532
-        }
17533
+    // Unrenders any visual indication of an event being resized
17534
+    TimeGrid.prototype.unrenderEventResize = function () {
17535
+        this.helperRenderer.unrender();
17536
     };
17537
-    EventPeriod.prototype.addEventDef = function (eventDef) {
17538
-        var eventDefsById = this.eventDefsById;
17539
-        var eventDefId = eventDef.id;
17540
-        var eventDefs = eventDefsById[eventDefId] || (eventDefsById[eventDefId] = []);
17541
-        var eventInstances = eventDef.buildInstances(this.unzonedRange);
17542
-        var i;
17543
-        eventDefs.push(eventDef);
17544
-        this.eventDefsByUid[eventDef.uid] = eventDef;
17545
-        for (i = 0; i < eventInstances.length; i++) {
17546
-            this.addEventInstance(eventInstances[i], eventDefId);
17547
+    /* Selection
17548
+    ------------------------------------------------------------------------------------------------------------------*/
17549
+    // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
17550
+    TimeGrid.prototype.renderSelectionFootprint = function (componentFootprint) {
17551
+        if (this.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
17552
+            this.helperRenderer.renderComponentFootprint(componentFootprint);
17553
+        }
17554
+        else {
17555
+            this.renderHighlight(componentFootprint);
17556
         }
17557
     };
17558
-    EventPeriod.prototype.removeEventDefsById = function (eventDefId) {
17559
-        var _this = this;
17560
-        this.getEventDefsById(eventDefId).forEach(function (eventDef) {
17561
-            _this.removeEventDef(eventDef);
17562
-        });
17563
+    // Unrenders any visual indication of a selection
17564
+    TimeGrid.prototype.unrenderSelection = function () {
17565
+        this.helperRenderer.unrender();
17566
+        this.unrenderHighlight();
17567
     };
17568
-    EventPeriod.prototype.removeAllEventDefs = function () {
17569
-        var isEmpty = $.isEmptyObject(this.eventDefsByUid);
17570
-        this.eventDefsByUid = {};
17571
-        this.eventDefsById = {};
17572
-        this.eventInstanceGroupsById = {};
17573
-        if (!isEmpty) {
17574
-            this.tryRelease();
17575
-        }
17576
+    return TimeGrid;
17577
+}(InteractiveDateComponent_1.default));
17578
+exports.default = TimeGrid;
17579
+TimeGrid.prototype.eventRendererClass = TimeGridEventRenderer_1.default;
17580
+TimeGrid.prototype.businessHourRendererClass = BusinessHourRenderer_1.default;
17581
+TimeGrid.prototype.helperRendererClass = TimeGridHelperRenderer_1.default;
17582
+TimeGrid.prototype.fillRendererClass = TimeGridFillRenderer_1.default;
17583
+StandardInteractionsMixin_1.default.mixInto(TimeGrid);
17584
+DayTableMixin_1.default.mixInto(TimeGrid);
17585
+
17586
+
17587
+/***/ }),
17588
+/* 240 */
17589
+/***/ (function(module, exports, __webpack_require__) {
17590
+
17591
+Object.defineProperty(exports, "__esModule", { value: true });
17592
+var tslib_1 = __webpack_require__(2);
17593
+var util_1 = __webpack_require__(4);
17594
+var EventRenderer_1 = __webpack_require__(44);
17595
+/*
17596
+Only handles foreground segs.
17597
+Does not own rendering. Use for low-level util methods by TimeGrid.
17598
+*/
17599
+var TimeGridEventRenderer = /** @class */ (function (_super) {
17600
+    tslib_1.__extends(TimeGridEventRenderer, _super);
17601
+    function TimeGridEventRenderer(timeGrid, fillRenderer) {
17602
+        var _this = _super.call(this, timeGrid, fillRenderer) || this;
17603
+        _this.timeGrid = timeGrid;
17604
+        return _this;
17605
+    }
17606
+    TimeGridEventRenderer.prototype.renderFgSegs = function (segs) {
17607
+        this.renderFgSegsIntoContainers(segs, this.timeGrid.fgContainerEls);
17608
     };
17609
-    EventPeriod.prototype.removeEventDef = function (eventDef) {
17610
-        var eventDefsById = this.eventDefsById;
17611
-        var eventDefs = eventDefsById[eventDef.id];
17612
-        delete this.eventDefsByUid[eventDef.uid];
17613
-        if (eventDefs) {
17614
-            util_1.removeExact(eventDefs, eventDef);
17615
-            if (!eventDefs.length) {
17616
-                delete eventDefsById[eventDef.id];
17617
-            }
17618
-            this.removeEventInstancesForDef(eventDef);
17619
+    // Given an array of foreground segments, render a DOM element for each, computes position,
17620
+    // and attaches to the column inner-container elements.
17621
+    TimeGridEventRenderer.prototype.renderFgSegsIntoContainers = function (segs, containerEls) {
17622
+        var segsByCol;
17623
+        var col;
17624
+        segsByCol = this.timeGrid.groupSegsByCol(segs);
17625
+        for (col = 0; col < this.timeGrid.colCnt; col++) {
17626
+            this.updateFgSegCoords(segsByCol[col]);
17627
         }
17628
+        this.timeGrid.attachSegsByCol(segsByCol, containerEls);
17629
     };
17630
-    // Event Instances
17631
-    // -----------------------------------------------------------------------------------------------------------------
17632
-    EventPeriod.prototype.getEventInstances = function () {
17633
-        var eventInstanceGroupsById = this.eventInstanceGroupsById;
17634
-        var eventInstances = [];
17635
-        var id;
17636
-        for (id in eventInstanceGroupsById) {
17637
-            eventInstances.push.apply(eventInstances, // append
17638
-            eventInstanceGroupsById[id].eventInstances);
17639
+    TimeGridEventRenderer.prototype.unrenderFgSegs = function () {
17640
+        if (this.fgSegs) { // hack
17641
+            this.fgSegs.forEach(function (seg) {
17642
+                seg.el.remove();
17643
+            });
17644
         }
17645
-        return eventInstances;
17646
     };
17647
-    EventPeriod.prototype.getEventInstancesWithId = function (eventDefId) {
17648
-        var eventInstanceGroup = this.eventInstanceGroupsById[eventDefId];
17649
-        if (eventInstanceGroup) {
17650
-            return eventInstanceGroup.eventInstances.slice(); // clone
17651
-        }
17652
-        return [];
17653
+    // Computes a default event time formatting string if `timeFormat` is not explicitly defined
17654
+    TimeGridEventRenderer.prototype.computeEventTimeFormat = function () {
17655
+        return this.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
17656
     };
17657
-    EventPeriod.prototype.getEventInstancesWithoutId = function (eventDefId) {
17658
-        var eventInstanceGroupsById = this.eventInstanceGroupsById;
17659
-        var matchingInstances = [];
17660
-        var id;
17661
-        for (id in eventInstanceGroupsById) {
17662
-            if (id !== eventDefId) {
17663
-                matchingInstances.push.apply(matchingInstances, // append
17664
-                eventInstanceGroupsById[id].eventInstances);
17665
+    // Computes a default `displayEventEnd` value if one is not expliclty defined
17666
+    TimeGridEventRenderer.prototype.computeDisplayEventEnd = function () {
17667
+        return true;
17668
+    };
17669
+    // Renders the HTML for a single event segment's default rendering
17670
+    TimeGridEventRenderer.prototype.fgSegHtml = function (seg, disableResizing) {
17671
+        var view = this.view;
17672
+        var calendar = view.calendar;
17673
+        var componentFootprint = seg.footprint.componentFootprint;
17674
+        var isAllDay = componentFootprint.isAllDay;
17675
+        var eventDef = seg.footprint.eventDef;
17676
+        var isDraggable = view.isEventDefDraggable(eventDef);
17677
+        var isResizableFromStart = !disableResizing && seg.isStart && view.isEventDefResizableFromStart(eventDef);
17678
+        var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventDefResizableFromEnd(eventDef);
17679
+        var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
17680
+        var skinCss = util_1.cssToStr(this.getSkinCss(eventDef));
17681
+        var timeText;
17682
+        var fullTimeText; // more verbose time text. for the print stylesheet
17683
+        var startTimeText; // just the start time text
17684
+        classes.unshift('fc-time-grid-event', 'fc-v-event');
17685
+        // if the event appears to span more than one day...
17686
+        if (view.isMultiDayRange(componentFootprint.unzonedRange)) {
17687
+            // Don't display time text on segments that run entirely through a day.
17688
+            // That would appear as midnight-midnight and would look dumb.
17689
+            // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
17690
+            if (seg.isStart || seg.isEnd) {
17691
+                var zonedStart = calendar.msToMoment(seg.startMs);
17692
+                var zonedEnd = calendar.msToMoment(seg.endMs);
17693
+                timeText = this._getTimeText(zonedStart, zonedEnd, isAllDay);
17694
+                fullTimeText = this._getTimeText(zonedStart, zonedEnd, isAllDay, 'LT');
17695
+                startTimeText = this._getTimeText(zonedStart, zonedEnd, isAllDay, null, false); // displayEnd=false
17696
             }
17697
         }
17698
-        return matchingInstances;
17699
+        else {
17700
+            // Display the normal time text for the *event's* times
17701
+            timeText = this.getTimeText(seg.footprint);
17702
+            fullTimeText = this.getTimeText(seg.footprint, 'LT');
17703
+            startTimeText = this.getTimeText(seg.footprint, null, false); // displayEnd=false
17704
+        }
17705
+        return '<a class="' + classes.join(' ') + '"' +
17706
+            (eventDef.url ?
17707
+                ' href="' + util_1.htmlEscape(eventDef.url) + '"' :
17708
+                '') +
17709
+            (skinCss ?
17710
+                ' style="' + skinCss + '"' :
17711
+                '') +
17712
+            '>' +
17713
+            '<div class="fc-content">' +
17714
+            (timeText ?
17715
+                '<div class="fc-time"' +
17716
+                    ' data-start="' + util_1.htmlEscape(startTimeText) + '"' +
17717
+                    ' data-full="' + util_1.htmlEscape(fullTimeText) + '"' +
17718
+                    '>' +
17719
+                    '<span>' + util_1.htmlEscape(timeText) + '</span>' +
17720
+                    '</div>' :
17721
+                '') +
17722
+            (eventDef.title ?
17723
+                '<div class="fc-title">' +
17724
+                    util_1.htmlEscape(eventDef.title) +
17725
+                    '</div>' :
17726
+                '') +
17727
+            '</div>' +
17728
+            '<div class="fc-bg"></div>' +
17729
+            /* TODO: write CSS for this
17730
+            (isResizableFromStart ?
17731
+              '<div class="fc-resizer fc-start-resizer"></div>' :
17732
+              ''
17733
+              ) +
17734
+            */
17735
+            (isResizableFromEnd ?
17736
+                '<div class="fc-resizer fc-end-resizer"></div>' :
17737
+                '') +
17738
+            '</a>';
17739
     };
17740
-    EventPeriod.prototype.addEventInstance = function (eventInstance, eventDefId) {
17741
-        var eventInstanceGroupsById = this.eventInstanceGroupsById;
17742
-        var eventInstanceGroup = eventInstanceGroupsById[eventDefId] ||
17743
-            (eventInstanceGroupsById[eventDefId] = new EventInstanceGroup_1.default());
17744
-        eventInstanceGroup.eventInstances.push(eventInstance);
17745
-        this.tryRelease();
17746
+    // Given segments that are assumed to all live in the *same column*,
17747
+    // compute their verical/horizontal coordinates and assign to their elements.
17748
+    TimeGridEventRenderer.prototype.updateFgSegCoords = function (segs) {
17749
+        this.timeGrid.computeSegVerticals(segs); // horizontals relies on this
17750
+        this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array
17751
+        this.timeGrid.assignSegVerticals(segs);
17752
+        this.assignFgSegHorizontals(segs);
17753
     };
17754
-    EventPeriod.prototype.removeEventInstancesForDef = function (eventDef) {
17755
-        var eventInstanceGroupsById = this.eventInstanceGroupsById;
17756
-        var eventInstanceGroup = eventInstanceGroupsById[eventDef.id];
17757
-        var removeCnt;
17758
-        if (eventInstanceGroup) {
17759
-            removeCnt = util_1.removeMatching(eventInstanceGroup.eventInstances, function (currentEventInstance) {
17760
-                return currentEventInstance.def === eventDef;
17761
-            });
17762
-            if (!eventInstanceGroup.eventInstances.length) {
17763
-                delete eventInstanceGroupsById[eventDef.id];
17764
+    // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
17765
+    // NOTE: Also reorders the given array by date!
17766
+    TimeGridEventRenderer.prototype.computeFgSegHorizontals = function (segs) {
17767
+        var levels;
17768
+        var level0;
17769
+        var i;
17770
+        this.sortEventSegs(segs); // order by certain criteria
17771
+        levels = buildSlotSegLevels(segs);
17772
+        computeForwardSlotSegs(levels);
17773
+        if ((level0 = levels[0])) {
17774
+            for (i = 0; i < level0.length; i++) {
17775
+                computeSlotSegPressures(level0[i]);
17776
             }
17777
-            if (removeCnt) {
17778
-                this.tryRelease();
17779
+            for (i = 0; i < level0.length; i++) {
17780
+                this.computeFgSegForwardBack(level0[i], 0, 0);
17781
             }
17782
         }
17783
     };
17784
-    // Releasing and Freezing
17785
-    // -----------------------------------------------------------------------------------------------------------------
17786
-    EventPeriod.prototype.tryRelease = function () {
17787
-        if (!this.pendingCnt) {
17788
-            if (!this.freezeDepth) {
17789
-                this.release();
17790
+    // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
17791
+    // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
17792
+    // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
17793
+    //
17794
+    // The segment might be part of a "series", which means consecutive segments with the same pressure
17795
+    // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
17796
+    // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
17797
+    // coordinate of the first segment in the series.
17798
+    TimeGridEventRenderer.prototype.computeFgSegForwardBack = function (seg, seriesBackwardPressure, seriesBackwardCoord) {
17799
+        var forwardSegs = seg.forwardSegs;
17800
+        var i;
17801
+        if (seg.forwardCoord === undefined) { // not already computed
17802
+            if (!forwardSegs.length) {
17803
+                // if there are no forward segments, this segment should butt up against the edge
17804
+                seg.forwardCoord = 1;
17805
             }
17806
             else {
17807
-                this.stuntedReleaseCnt++;
17808
+                // sort highest pressure first
17809
+                this.sortForwardSegs(forwardSegs);
17810
+                // this segment's forwardCoord will be calculated from the backwardCoord of the
17811
+                // highest-pressure forward segment.
17812
+                this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
17813
+                seg.forwardCoord = forwardSegs[0].backwardCoord;
17814
+            }
17815
+            // calculate the backwardCoord from the forwardCoord. consider the series
17816
+            seg.backwardCoord = seg.forwardCoord -
17817
+                (seg.forwardCoord - seriesBackwardCoord) / // available width for series
17818
+                    (seriesBackwardPressure + 1); // # of segments in the series
17819
+            // use this segment's coordinates to computed the coordinates of the less-pressurized
17820
+            // forward segments
17821
+            for (i = 0; i < forwardSegs.length; i++) {
17822
+                this.computeFgSegForwardBack(forwardSegs[i], 0, seg.forwardCoord);
17823
             }
17824
         }
17825
     };
17826
-    EventPeriod.prototype.release = function () {
17827
-        this.releaseCnt++;
17828
-        this.trigger('release', this.eventInstanceGroupsById);
17829
+    TimeGridEventRenderer.prototype.sortForwardSegs = function (forwardSegs) {
17830
+        forwardSegs.sort(util_1.proxy(this, 'compareForwardSegs'));
17831
     };
17832
-    EventPeriod.prototype.whenReleased = function () {
17833
-        var _this = this;
17834
-        if (this.releaseCnt) {
17835
-            return Promise_1.default.resolve(this.eventInstanceGroupsById);
17836
+    // A cmp function for determining which forward segment to rely on more when computing coordinates.
17837
+    TimeGridEventRenderer.prototype.compareForwardSegs = function (seg1, seg2) {
17838
+        // put higher-pressure first
17839
+        return seg2.forwardPressure - seg1.forwardPressure ||
17840
+            // put segments that are closer to initial edge first (and favor ones with no coords yet)
17841
+            (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
17842
+            // do normal sorting...
17843
+            this.compareEventSegs(seg1, seg2);
17844
+    };
17845
+    // Given foreground event segments that have already had their position coordinates computed,
17846
+    // assigns position-related CSS values to their elements.
17847
+    TimeGridEventRenderer.prototype.assignFgSegHorizontals = function (segs) {
17848
+        var i;
17849
+        var seg;
17850
+        for (i = 0; i < segs.length; i++) {
17851
+            seg = segs[i];
17852
+            seg.el.css(this.generateFgSegHorizontalCss(seg));
17853
+            // if the event is short that the title will be cut off,
17854
+            // attach a className that condenses the title into the time area.
17855
+            if (seg.footprint.eventDef.title && seg.bottom - seg.top < 30) {
17856
+                seg.el.addClass('fc-short'); // TODO: "condensed" is a better name
17857
+            }
17858
+        }
17859
+    };
17860
+    // Generates an object with CSS properties/values that should be applied to an event segment element.
17861
+    // Contains important positioning-related properties that should be applied to any event element, customized or not.
17862
+    TimeGridEventRenderer.prototype.generateFgSegHorizontalCss = function (seg) {
17863
+        var shouldOverlap = this.opt('slotEventOverlap');
17864
+        var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
17865
+        var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
17866
+        var props = this.timeGrid.generateSegVerticalCss(seg); // get top/bottom first
17867
+        var isRTL = this.timeGrid.isRTL;
17868
+        var left; // amount of space from left edge, a fraction of the total width
17869
+        var right; // amount of space from right edge, a fraction of the total width
17870
+        if (shouldOverlap) {
17871
+            // double the width, but don't go beyond the maximum forward coordinate (1.0)
17872
+            forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
17873
+        }
17874
+        if (isRTL) {
17875
+            left = 1 - forwardCoord;
17876
+            right = backwardCoord;
17877
         }
17878
         else {
17879
-            return Promise_1.default.construct(function (onResolve) {
17880
-                _this.one('release', onResolve);
17881
-            });
17882
+            left = backwardCoord;
17883
+            right = 1 - forwardCoord;
17884
         }
17885
-    };
17886
-    EventPeriod.prototype.freeze = function () {
17887
-        if (!(this.freezeDepth++)) {
17888
-            this.stuntedReleaseCnt = 0;
17889
+        props.zIndex = seg.level + 1; // convert from 0-base to 1-based
17890
+        props.left = left * 100 + '%';
17891
+        props.right = right * 100 + '%';
17892
+        if (shouldOverlap && seg.forwardPressure) {
17893
+            // add padding to the edge so that forward stacked events don't cover the resizer's icon
17894
+            props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
17895
         }
17896
+        return props;
17897
     };
17898
-    EventPeriod.prototype.thaw = function () {
17899
-        if (!(--this.freezeDepth) && this.stuntedReleaseCnt && !this.pendingCnt) {
17900
-            this.release();
17901
+    return TimeGridEventRenderer;
17902
+}(EventRenderer_1.default));
17903
+exports.default = TimeGridEventRenderer;
17904
+// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
17905
+// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
17906
+function buildSlotSegLevels(segs) {
17907
+    var levels = [];
17908
+    var i;
17909
+    var seg;
17910
+    var j;
17911
+    for (i = 0; i < segs.length; i++) {
17912
+        seg = segs[i];
17913
+        // go through all the levels and stop on the first level where there are no collisions
17914
+        for (j = 0; j < levels.length; j++) {
17915
+            if (!computeSlotSegCollisions(seg, levels[j]).length) {
17916
+                break;
17917
+            }
17918
         }
17919
-    };
17920
-    return EventPeriod;
17921
-}());
17922
-exports.default = EventPeriod;
17923
-EmitterMixin_1.default.mixInto(EventPeriod);
17924
+        seg.level = j;
17925
+        (levels[j] || (levels[j] = [])).push(seg);
17926
+    }
17927
+    return levels;
17928
+}
17929
+// For every segment, figure out the other segments that are in subsequent
17930
+// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
17931
+function computeForwardSlotSegs(levels) {
17932
+    var i;
17933
+    var level;
17934
+    var j;
17935
+    var seg;
17936
+    var k;
17937
+    for (i = 0; i < levels.length; i++) {
17938
+        level = levels[i];
17939
+        for (j = 0; j < level.length; j++) {
17940
+            seg = level[j];
17941
+            seg.forwardSegs = [];
17942
+            for (k = i + 1; k < levels.length; k++) {
17943
+                computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
17944
+            }
17945
+        }
17946
+    }
17947
+}
17948
+// Figure out which path forward (via seg.forwardSegs) results in the longest path until
17949
+// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
17950
+function computeSlotSegPressures(seg) {
17951
+    var forwardSegs = seg.forwardSegs;
17952
+    var forwardPressure = 0;
17953
+    var i;
17954
+    var forwardSeg;
17955
+    if (seg.forwardPressure === undefined) { // not already computed
17956
+        for (i = 0; i < forwardSegs.length; i++) {
17957
+            forwardSeg = forwardSegs[i];
17958
+            // figure out the child's maximum forward path
17959
+            computeSlotSegPressures(forwardSeg);
17960
+            // either use the existing maximum, or use the child's forward pressure
17961
+            // plus one (for the forwardSeg itself)
17962
+            forwardPressure = Math.max(forwardPressure, 1 + forwardSeg.forwardPressure);
17963
+        }
17964
+        seg.forwardPressure = forwardPressure;
17965
+    }
17966
+}
17967
+// Find all the segments in `otherSegs` that vertically collide with `seg`.
17968
+// Append into an optionally-supplied `results` array and return.
17969
+function computeSlotSegCollisions(seg, otherSegs, results) {
17970
+    if (results === void 0) { results = []; }
17971
+    for (var i = 0; i < otherSegs.length; i++) {
17972
+        if (isSlotSegCollision(seg, otherSegs[i])) {
17973
+            results.push(otherSegs[i]);
17974
+        }
17975
+    }
17976
+    return results;
17977
+}
17978
+// Do these segments occupy the same vertical space?
17979
+function isSlotSegCollision(seg1, seg2) {
17980
+    return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
17981
+}
17982
 
17983
 
17984
 /***/ }),
17985
-/* 244 */
17986
+/* 241 */
17987
 /***/ (function(module, exports, __webpack_require__) {
17988
 
17989
 Object.defineProperty(exports, "__esModule", { value: true });
17990
+var tslib_1 = __webpack_require__(2);
17991
 var $ = __webpack_require__(3);
17992
-var util_1 = __webpack_require__(4);
17993
-var ListenerMixin_1 = __webpack_require__(7);
17994
-/* Creates a clone of an element and lets it track the mouse as it moves
17995
-----------------------------------------------------------------------------------------------------------------------*/
17996
-var MouseFollower = /** @class */ (function () {
17997
-    function MouseFollower(sourceEl, options) {
17998
-        this.isFollowing = false;
17999
-        this.isHidden = false;
18000
-        this.isAnimating = false; // doing the revert animation?
18001
-        this.options = options = options || {};
18002
-        this.sourceEl = sourceEl;
18003
-        this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
18004
+var HelperRenderer_1 = __webpack_require__(63);
18005
+var TimeGridHelperRenderer = /** @class */ (function (_super) {
18006
+    tslib_1.__extends(TimeGridHelperRenderer, _super);
18007
+    function TimeGridHelperRenderer() {
18008
+        return _super !== null && _super.apply(this, arguments) || this;
18009
     }
18010
-    // Causes the element to start following the mouse
18011
-    MouseFollower.prototype.start = function (ev) {
18012
-        if (!this.isFollowing) {
18013
-            this.isFollowing = true;
18014
-            this.y0 = util_1.getEvY(ev);
18015
-            this.x0 = util_1.getEvX(ev);
18016
-            this.topDelta = 0;
18017
-            this.leftDelta = 0;
18018
-            if (!this.isHidden) {
18019
-                this.updatePosition();
18020
-            }
18021
-            if (util_1.getEvIsTouch(ev)) {
18022
-                this.listenTo($(document), 'touchmove', this.handleMove);
18023
-            }
18024
-            else {
18025
-                this.listenTo($(document), 'mousemove', this.handleMove);
18026
-            }
18027
-        }
18028
-    };
18029
-    // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
18030
-    // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
18031
-    MouseFollower.prototype.stop = function (shouldRevert, callback) {
18032
-        var _this = this;
18033
-        var revertDuration = this.options.revertDuration;
18034
-        var complete = function () {
18035
-            _this.isAnimating = false;
18036
-            _this.removeElement();
18037
-            _this.top0 = _this.left0 = null; // reset state for future updatePosition calls
18038
-            if (callback) {
18039
-                callback();
18040
-            }
18041
-        };
18042
-        if (this.isFollowing && !this.isAnimating) {
18043
-            this.isFollowing = false;
18044
-            this.stopListeningTo($(document));
18045
-            if (shouldRevert && revertDuration && !this.isHidden) {
18046
-                this.isAnimating = true;
18047
-                this.el.animate({
18048
-                    top: this.top0,
18049
-                    left: this.left0
18050
-                }, {
18051
-                    duration: revertDuration,
18052
-                    complete: complete
18053
+    TimeGridHelperRenderer.prototype.renderSegs = function (segs, sourceSeg) {
18054
+        var helperNodes = [];
18055
+        var i;
18056
+        var seg;
18057
+        var sourceEl;
18058
+        // TODO: not good to call eventRenderer this way
18059
+        this.eventRenderer.renderFgSegsIntoContainers(segs, this.component.helperContainerEls);
18060
+        // Try to make the segment that is in the same row as sourceSeg look the same
18061
+        for (i = 0; i < segs.length; i++) {
18062
+            seg = segs[i];
18063
+            if (sourceSeg && sourceSeg.col === seg.col) {
18064
+                sourceEl = sourceSeg.el;
18065
+                seg.el.css({
18066
+                    left: sourceEl.css('left'),
18067
+                    right: sourceEl.css('right'),
18068
+                    'margin-left': sourceEl.css('margin-left'),
18069
+                    'margin-right': sourceEl.css('margin-right')
18070
                 });
18071
             }
18072
-            else {
18073
-                complete();
18074
-            }
18075
+            helperNodes.push(seg.el[0]);
18076
         }
18077
+        return $(helperNodes); // must return the elements rendered
18078
     };
18079
-    // Gets the tracking element. Create it if necessary
18080
-    MouseFollower.prototype.getEl = function () {
18081
-        var el = this.el;
18082
-        if (!el) {
18083
-            el = this.el = this.sourceEl.clone()
18084
-                .addClass(this.options.additionalClass || '')
18085
-                .css({
18086
-                position: 'absolute',
18087
-                visibility: '',
18088
-                display: this.isHidden ? 'none' : '',
18089
-                margin: 0,
18090
-                right: 'auto',
18091
-                bottom: 'auto',
18092
-                width: this.sourceEl.width(),
18093
-                height: this.sourceEl.height(),
18094
-                opacity: this.options.opacity || '',
18095
-                zIndex: this.options.zIndex
18096
-            });
18097
-            // we don't want long taps or any mouse interaction causing selection/menus.
18098
-            // would use preventSelection(), but that prevents selectstart, causing problems.
18099
-            el.addClass('fc-unselectable');
18100
-            el.appendTo(this.parentEl);
18101
+    return TimeGridHelperRenderer;
18102
+}(HelperRenderer_1.default));
18103
+exports.default = TimeGridHelperRenderer;
18104
+
18105
+
18106
+/***/ }),
18107
+/* 242 */
18108
+/***/ (function(module, exports, __webpack_require__) {
18109
+
18110
+Object.defineProperty(exports, "__esModule", { value: true });
18111
+var tslib_1 = __webpack_require__(2);
18112
+var FillRenderer_1 = __webpack_require__(62);
18113
+var TimeGridFillRenderer = /** @class */ (function (_super) {
18114
+    tslib_1.__extends(TimeGridFillRenderer, _super);
18115
+    function TimeGridFillRenderer() {
18116
+        return _super !== null && _super.apply(this, arguments) || this;
18117
+    }
18118
+    TimeGridFillRenderer.prototype.attachSegEls = function (type, segs) {
18119
+        var timeGrid = this.component;
18120
+        var containerEls;
18121
+        // TODO: more efficient lookup
18122
+        if (type === 'bgEvent') {
18123
+            containerEls = timeGrid.bgContainerEls;
18124
         }
18125
-        return el;
18126
-    };
18127
-    // Removes the tracking element if it has already been created
18128
-    MouseFollower.prototype.removeElement = function () {
18129
-        if (this.el) {
18130
-            this.el.remove();
18131
-            this.el = null;
18132
+        else if (type === 'businessHours') {
18133
+            containerEls = timeGrid.businessContainerEls;
18134
         }
18135
-    };
18136
-    // Update the CSS position of the tracking element
18137
-    MouseFollower.prototype.updatePosition = function () {
18138
-        var sourceOffset;
18139
-        var origin;
18140
-        this.getEl(); // ensure this.el
18141
-        // make sure origin info was computed
18142
-        if (this.top0 == null) {
18143
-            sourceOffset = this.sourceEl.offset();
18144
-            origin = this.el.offsetParent().offset();
18145
-            this.top0 = sourceOffset.top - origin.top;
18146
-            this.left0 = sourceOffset.left - origin.left;
18147
+        else if (type === 'highlight') {
18148
+            containerEls = timeGrid.highlightContainerEls;
18149
         }
18150
-        this.el.css({
18151
-            top: this.top0 + this.topDelta,
18152
-            left: this.left0 + this.leftDelta
18153
+        timeGrid.updateSegVerticals(segs);
18154
+        timeGrid.attachSegsByCol(timeGrid.groupSegsByCol(segs), containerEls);
18155
+        return segs.map(function (seg) {
18156
+            return seg.el[0];
18157
         });
18158
     };
18159
-    // Gets called when the user moves the mouse
18160
-    MouseFollower.prototype.handleMove = function (ev) {
18161
-        this.topDelta = util_1.getEvY(ev) - this.y0;
18162
-        this.leftDelta = util_1.getEvX(ev) - this.x0;
18163
-        if (!this.isHidden) {
18164
-            this.updatePosition();
18165
-        }
18166
-    };
18167
-    // Temporarily makes the tracking element invisible. Can be called before following starts
18168
-    MouseFollower.prototype.hide = function () {
18169
-        if (!this.isHidden) {
18170
-            this.isHidden = true;
18171
-            if (this.el) {
18172
-                this.el.hide();
18173
-            }
18174
-        }
18175
-    };
18176
-    // Show the tracking element after it has been temporarily hidden
18177
-    MouseFollower.prototype.show = function () {
18178
-        if (this.isHidden) {
18179
-            this.isHidden = false;
18180
-            this.updatePosition();
18181
-            this.getEl().show();
18182
-        }
18183
-    };
18184
-    return MouseFollower;
18185
-}());
18186
-exports.default = MouseFollower;
18187
-ListenerMixin_1.default.mixInto(MouseFollower);
18188
+    return TimeGridFillRenderer;
18189
+}(FillRenderer_1.default));
18190
+exports.default = TimeGridFillRenderer;
18191
 
18192
 
18193
 /***/ }),
18194
-/* 245 */
18195
+/* 243 */
18196
 /***/ (function(module, exports, __webpack_require__) {
18197
 
18198
 Object.defineProperty(exports, "__esModule", { value: true });
18199
 var tslib_1 = __webpack_require__(2);
18200
-var HitDragListener_1 = __webpack_require__(23);
18201
-var Interaction_1 = __webpack_require__(15);
18202
-var DateClicking = /** @class */ (function (_super) {
18203
-    tslib_1.__extends(DateClicking, _super);
18204
-    /*
18205
-    component must implement:
18206
-      - bindDateHandlerToEl
18207
-      - getSafeHitFootprint
18208
-      - getHitEl
18209
-    */
18210
-    function DateClicking(component) {
18211
-        var _this = _super.call(this, component) || this;
18212
-        _this.dragListener = _this.buildDragListener();
18213
+var $ = __webpack_require__(3);
18214
+var util_1 = __webpack_require__(4);
18215
+var EventRenderer_1 = __webpack_require__(44);
18216
+/* Event-rendering methods for the DayGrid class
18217
+----------------------------------------------------------------------------------------------------------------------*/
18218
+var DayGridEventRenderer = /** @class */ (function (_super) {
18219
+    tslib_1.__extends(DayGridEventRenderer, _super);
18220
+    function DayGridEventRenderer(dayGrid, fillRenderer) {
18221
+        var _this = _super.call(this, dayGrid, fillRenderer) || this;
18222
+        _this.dayGrid = dayGrid;
18223
         return _this;
18224
     }
18225
-    DateClicking.prototype.end = function () {
18226
-        this.dragListener.endInteraction();
18227
-    };
18228
-    DateClicking.prototype.bindToEl = function (el) {
18229
-        var component = this.component;
18230
-        var dragListener = this.dragListener;
18231
-        component.bindDateHandlerToEl(el, 'mousedown', function (ev) {
18232
-            if (!component.shouldIgnoreMouse()) {
18233
-                dragListener.startInteraction(ev);
18234
-            }
18235
+    DayGridEventRenderer.prototype.renderBgRanges = function (eventRanges) {
18236
+        // don't render timed background events
18237
+        eventRanges = $.grep(eventRanges, function (eventRange) {
18238
+            return eventRange.eventDef.isAllDay();
18239
         });
18240
-        component.bindDateHandlerToEl(el, 'touchstart', function (ev) {
18241
-            if (!component.shouldIgnoreTouch()) {
18242
-                dragListener.startInteraction(ev);
18243
-            }
18244
+        _super.prototype.renderBgRanges.call(this, eventRanges);
18245
+    };
18246
+    // Renders the given foreground event segments onto the grid
18247
+    DayGridEventRenderer.prototype.renderFgSegs = function (segs) {
18248
+        var rowStructs = this.rowStructs = this.renderSegRows(segs);
18249
+        // append to each row's content skeleton
18250
+        this.dayGrid.rowEls.each(function (i, rowNode) {
18251
+            $(rowNode).find('.fc-content-skeleton > table').append(rowStructs[i].tbodyEl);
18252
         });
18253
     };
18254
-    // Creates a listener that tracks the user's drag across day elements, for day clicking.
18255
-    DateClicking.prototype.buildDragListener = function () {
18256
-        var _this = this;
18257
-        var component = this.component;
18258
-        var dayClickHit; // null if invalid dayClick
18259
-        var dragListener = new HitDragListener_1.default(component, {
18260
-            scroll: this.opt('dragScroll'),
18261
-            interactionStart: function () {
18262
-                dayClickHit = dragListener.origHit;
18263
-            },
18264
-            hitOver: function (hit, isOrig, origHit) {
18265
-                // if user dragged to another cell at any point, it can no longer be a dayClick
18266
-                if (!isOrig) {
18267
-                    dayClickHit = null;
18268
+    // Unrenders all currently rendered foreground event segments
18269
+    DayGridEventRenderer.prototype.unrenderFgSegs = function () {
18270
+        var rowStructs = this.rowStructs || [];
18271
+        var rowStruct;
18272
+        while ((rowStruct = rowStructs.pop())) {
18273
+            rowStruct.tbodyEl.remove();
18274
+        }
18275
+        this.rowStructs = null;
18276
+    };
18277
+    // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
18278
+    // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
18279
+    // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
18280
+    DayGridEventRenderer.prototype.renderSegRows = function (segs) {
18281
+        var rowStructs = [];
18282
+        var segRows;
18283
+        var row;
18284
+        segRows = this.groupSegRows(segs); // group into nested arrays
18285
+        // iterate each row of segment groupings
18286
+        for (row = 0; row < segRows.length; row++) {
18287
+            rowStructs.push(this.renderSegRow(row, segRows[row]));
18288
+        }
18289
+        return rowStructs;
18290
+    };
18291
+    // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
18292
+    // the segments. Returns object with a bunch of internal data about how the render was calculated.
18293
+    // NOTE: modifies rowSegs
18294
+    DayGridEventRenderer.prototype.renderSegRow = function (row, rowSegs) {
18295
+        var colCnt = this.dayGrid.colCnt;
18296
+        var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
18297
+        var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
18298
+        var tbody = $('<tbody>');
18299
+        var segMatrix = []; // lookup for which segments are rendered into which level+col cells
18300
+        var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
18301
+        var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
18302
+        var i;
18303
+        var levelSegs;
18304
+        var col;
18305
+        var tr;
18306
+        var j;
18307
+        var seg;
18308
+        var td;
18309
+        // populates empty cells from the current column (`col`) to `endCol`
18310
+        function emptyCellsUntil(endCol) {
18311
+            while (col < endCol) {
18312
+                // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
18313
+                td = (loneCellMatrix[i - 1] || [])[col];
18314
+                if (td) {
18315
+                    td.attr('rowspan', parseInt(td.attr('rowspan') || 1, 10) + 1);
18316
                 }
18317
-            },
18318
-            hitOut: function () {
18319
-                dayClickHit = null;
18320
-            },
18321
-            interactionEnd: function (ev, isCancelled) {
18322
-                var componentFootprint;
18323
-                if (!isCancelled && dayClickHit) {
18324
-                    componentFootprint = component.getSafeHitFootprint(dayClickHit);
18325
-                    if (componentFootprint) {
18326
-                        _this.view.triggerDayClick(componentFootprint, component.getHitEl(dayClickHit), ev);
18327
+                else {
18328
+                    td = $('<td>');
18329
+                    tr.append(td);
18330
+                }
18331
+                cellMatrix[i][col] = td;
18332
+                loneCellMatrix[i][col] = td;
18333
+                col++;
18334
+            }
18335
+        }
18336
+        for (i = 0; i < levelCnt; i++) { // iterate through all levels
18337
+            levelSegs = segLevels[i];
18338
+            col = 0;
18339
+            tr = $('<tr>');
18340
+            segMatrix.push([]);
18341
+            cellMatrix.push([]);
18342
+            loneCellMatrix.push([]);
18343
+            // levelCnt might be 1 even though there are no actual levels. protect against this.
18344
+            // this single empty row is useful for styling.
18345
+            if (levelSegs) {
18346
+                for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
18347
+                    seg = levelSegs[j];
18348
+                    emptyCellsUntil(seg.leftCol);
18349
+                    // create a container that occupies or more columns. append the event element.
18350
+                    td = $('<td class="fc-event-container">').append(seg.el);
18351
+                    if (seg.leftCol !== seg.rightCol) {
18352
+                        td.attr('colspan', seg.rightCol - seg.leftCol + 1);
18353
+                    }
18354
+                    else { // a single-column segment
18355
+                        loneCellMatrix[i][col] = td;
18356
+                    }
18357
+                    while (col <= seg.rightCol) {
18358
+                        cellMatrix[i][col] = td;
18359
+                        segMatrix[i][col] = seg;
18360
+                        col++;
18361
                     }
18362
+                    tr.append(td);
18363
                 }
18364
             }
18365
-        });
18366
-        // because dragListener won't be called with any time delay, "dragging" will begin immediately,
18367
-        // which will kill any touchmoving/scrolling. Prevent this.
18368
-        dragListener.shouldCancelTouchScroll = false;
18369
-        dragListener.scrollAlwaysKills = true;
18370
-        return dragListener;
18371
-    };
18372
-    return DateClicking;
18373
-}(Interaction_1.default));
18374
-exports.default = DateClicking;
18375
-
18376
-
18377
-/***/ }),
18378
-/* 246 */
18379
-/***/ (function(module, exports, __webpack_require__) {
18380
-
18381
-Object.defineProperty(exports, "__esModule", { value: true });
18382
-var tslib_1 = __webpack_require__(2);
18383
-var util_1 = __webpack_require__(4);
18384
-var EventRenderer_1 = __webpack_require__(42);
18385
-/*
18386
-Only handles foreground segs.
18387
-Does not own rendering. Use for low-level util methods by TimeGrid.
18388
-*/
18389
-var TimeGridEventRenderer = /** @class */ (function (_super) {
18390
-    tslib_1.__extends(TimeGridEventRenderer, _super);
18391
-    function TimeGridEventRenderer(timeGrid, fillRenderer) {
18392
-        var _this = _super.call(this, timeGrid, fillRenderer) || this;
18393
-        _this.timeGrid = timeGrid;
18394
-        return _this;
18395
-    }
18396
-    TimeGridEventRenderer.prototype.renderFgSegs = function (segs) {
18397
-        this.renderFgSegsIntoContainers(segs, this.timeGrid.fgContainerEls);
18398
+            emptyCellsUntil(colCnt); // finish off the row
18399
+            this.dayGrid.bookendCells(tr);
18400
+            tbody.append(tr);
18401
+        }
18402
+        return {
18403
+            row: row,
18404
+            tbodyEl: tbody,
18405
+            cellMatrix: cellMatrix,
18406
+            segMatrix: segMatrix,
18407
+            segLevels: segLevels,
18408
+            segs: rowSegs
18409
+        };
18410
     };
18411
-    // Given an array of foreground segments, render a DOM element for each, computes position,
18412
-    // and attaches to the column inner-container elements.
18413
-    TimeGridEventRenderer.prototype.renderFgSegsIntoContainers = function (segs, containerEls) {
18414
-        var segsByCol;
18415
-        var col;
18416
-        segsByCol = this.timeGrid.groupSegsByCol(segs);
18417
-        for (col = 0; col < this.timeGrid.colCnt; col++) {
18418
-            this.updateFgSegCoords(segsByCol[col]);
18419
+    // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
18420
+    // NOTE: modifies segs
18421
+    DayGridEventRenderer.prototype.buildSegLevels = function (segs) {
18422
+        var levels = [];
18423
+        var i;
18424
+        var seg;
18425
+        var j;
18426
+        // Give preference to elements with certain criteria, so they have
18427
+        // a chance to be closer to the top.
18428
+        this.sortEventSegs(segs);
18429
+        for (i = 0; i < segs.length; i++) {
18430
+            seg = segs[i];
18431
+            // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
18432
+            for (j = 0; j < levels.length; j++) {
18433
+                if (!isDaySegCollision(seg, levels[j])) {
18434
+                    break;
18435
+                }
18436
+            }
18437
+            // `j` now holds the desired subrow index
18438
+            seg.level = j;
18439
+            // create new level array if needed and append segment
18440
+            (levels[j] || (levels[j] = [])).push(seg);
18441
         }
18442
-        this.timeGrid.attachSegsByCol(segsByCol, containerEls);
18443
+        // order segments left-to-right. very important if calendar is RTL
18444
+        for (j = 0; j < levels.length; j++) {
18445
+            levels[j].sort(compareDaySegCols);
18446
+        }
18447
+        return levels;
18448
     };
18449
-    TimeGridEventRenderer.prototype.unrenderFgSegs = function () {
18450
-        if (this.fgSegs) {
18451
-            this.fgSegs.forEach(function (seg) {
18452
-                seg.el.remove();
18453
-            });
18454
+    // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
18455
+    DayGridEventRenderer.prototype.groupSegRows = function (segs) {
18456
+        var segRows = [];
18457
+        var i;
18458
+        for (i = 0; i < this.dayGrid.rowCnt; i++) {
18459
+            segRows.push([]);
18460
         }
18461
+        for (i = 0; i < segs.length; i++) {
18462
+            segRows[segs[i].row].push(segs[i]);
18463
+        }
18464
+        return segRows;
18465
     };
18466
     // Computes a default event time formatting string if `timeFormat` is not explicitly defined
18467
-    TimeGridEventRenderer.prototype.computeEventTimeFormat = function () {
18468
-        return this.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
18469
+    DayGridEventRenderer.prototype.computeEventTimeFormat = function () {
18470
+        return this.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
18471
     };
18472
     // Computes a default `displayEventEnd` value if one is not expliclty defined
18473
-    TimeGridEventRenderer.prototype.computeDisplayEventEnd = function () {
18474
-        return true;
18475
+    DayGridEventRenderer.prototype.computeDisplayEventEnd = function () {
18476
+        return this.dayGrid.colCnt === 1; // we'll likely have space if there's only one day
18477
     };
18478
-    // Renders the HTML for a single event segment's default rendering
18479
-    TimeGridEventRenderer.prototype.fgSegHtml = function (seg, disableResizing) {
18480
+    // Builds the HTML to be used for the default element for an individual segment
18481
+    DayGridEventRenderer.prototype.fgSegHtml = function (seg, disableResizing) {
18482
         var view = this.view;
18483
-        var calendar = view.calendar;
18484
-        var componentFootprint = seg.footprint.componentFootprint;
18485
-        var isAllDay = componentFootprint.isAllDay;
18486
         var eventDef = seg.footprint.eventDef;
18487
+        var isAllDay = seg.footprint.componentFootprint.isAllDay;
18488
         var isDraggable = view.isEventDefDraggable(eventDef);
18489
-        var isResizableFromStart = !disableResizing && seg.isStart && view.isEventDefResizableFromStart(eventDef);
18490
-        var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventDefResizableFromEnd(eventDef);
18491
+        var isResizableFromStart = !disableResizing && isAllDay &&
18492
+            seg.isStart && view.isEventDefResizableFromStart(eventDef);
18493
+        var isResizableFromEnd = !disableResizing && isAllDay &&
18494
+            seg.isEnd && view.isEventDefResizableFromEnd(eventDef);
18495
         var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
18496
         var skinCss = util_1.cssToStr(this.getSkinCss(eventDef));
18497
+        var timeHtml = '';
18498
         var timeText;
18499
-        var fullTimeText; // more verbose time text. for the print stylesheet
18500
-        var startTimeText; // just the start time text
18501
-        classes.unshift('fc-time-grid-event', 'fc-v-event');
18502
-        // if the event appears to span more than one day...
18503
-        if (view.isMultiDayRange(componentFootprint.unzonedRange)) {
18504
-            // Don't display time text on segments that run entirely through a day.
18505
-            // That would appear as midnight-midnight and would look dumb.
18506
-            // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
18507
-            if (seg.isStart || seg.isEnd) {
18508
-                var zonedStart = calendar.msToMoment(seg.startMs);
18509
-                var zonedEnd = calendar.msToMoment(seg.endMs);
18510
-                timeText = this._getTimeText(zonedStart, zonedEnd, isAllDay);
18511
-                fullTimeText = this._getTimeText(zonedStart, zonedEnd, isAllDay, 'LT');
18512
-                startTimeText = this._getTimeText(zonedStart, zonedEnd, isAllDay, null, false); // displayEnd=false
18513
-            }
18514
-        }
18515
-        else {
18516
-            // Display the normal time text for the *event's* times
18517
+        var titleHtml;
18518
+        classes.unshift('fc-day-grid-event', 'fc-h-event');
18519
+        // Only display a timed events time if it is the starting segment
18520
+        if (seg.isStart) {
18521
             timeText = this.getTimeText(seg.footprint);
18522
-            fullTimeText = this.getTimeText(seg.footprint, 'LT');
18523
-            startTimeText = this.getTimeText(seg.footprint, null, false); // displayEnd=false
18524
+            if (timeText) {
18525
+                timeHtml = '<span class="fc-time">' + util_1.htmlEscape(timeText) + '</span>';
18526
+            }
18527
         }
18528
+        titleHtml =
18529
+            '<span class="fc-title">' +
18530
+                (util_1.htmlEscape(eventDef.title || '') || '&nbsp;') + // we always want one line of height
18531
+                '</span>';
18532
         return '<a class="' + classes.join(' ') + '"' +
18533
             (eventDef.url ?
18534
                 ' href="' + util_1.htmlEscape(eventDef.url) + '"' :
18535
@@ -13817,450 +13872,461 @@
18536
                 '') +
18537
             '>' +
18538
             '<div class="fc-content">' +
18539
-            (timeText ?
18540
-                '<div class="fc-time"' +
18541
-                    ' data-start="' + util_1.htmlEscape(startTimeText) + '"' +
18542
-                    ' data-full="' + util_1.htmlEscape(fullTimeText) + '"' +
18543
-                    '>' +
18544
-                    '<span>' + util_1.htmlEscape(timeText) + '</span>' +
18545
-                    '</div>' :
18546
-                '') +
18547
-            (eventDef.title ?
18548
-                '<div class="fc-title">' +
18549
-                    util_1.htmlEscape(eventDef.title) +
18550
-                    '</div>' :
18551
-                '') +
18552
+            (this.dayGrid.isRTL ?
18553
+                titleHtml + ' ' + timeHtml : // put a natural space in between
18554
+                timeHtml + ' ' + titleHtml //
18555
+            ) +
18556
             '</div>' +
18557
-            '<div class="fc-bg"/>' +
18558
-            /* TODO: write CSS for this
18559
             (isResizableFromStart ?
18560
-              '<div class="fc-resizer fc-start-resizer" />' :
18561
-              ''
18562
-              ) +
18563
-            */
18564
+                '<div class="fc-resizer fc-start-resizer"></div>' :
18565
+                '') +
18566
             (isResizableFromEnd ?
18567
-                '<div class="fc-resizer fc-end-resizer" />' :
18568
+                '<div class="fc-resizer fc-end-resizer"></div>' :
18569
                 '') +
18570
             '</a>';
18571
     };
18572
-    // Given segments that are assumed to all live in the *same column*,
18573
-    // compute their verical/horizontal coordinates and assign to their elements.
18574
-    TimeGridEventRenderer.prototype.updateFgSegCoords = function (segs) {
18575
-        this.timeGrid.computeSegVerticals(segs); // horizontals relies on this
18576
-        this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array
18577
-        this.timeGrid.assignSegVerticals(segs);
18578
-        this.assignFgSegHorizontals(segs);
18579
-    };
18580
-    // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
18581
-    // NOTE: Also reorders the given array by date!
18582
-    TimeGridEventRenderer.prototype.computeFgSegHorizontals = function (segs) {
18583
-        var levels;
18584
-        var level0;
18585
-        var i;
18586
-        this.sortEventSegs(segs); // order by certain criteria
18587
-        levels = buildSlotSegLevels(segs);
18588
-        computeForwardSlotSegs(levels);
18589
-        if ((level0 = levels[0])) {
18590
-            for (i = 0; i < level0.length; i++) {
18591
-                computeSlotSegPressures(level0[i]);
18592
-            }
18593
-            for (i = 0; i < level0.length; i++) {
18594
-                this.computeFgSegForwardBack(level0[i], 0, 0);
18595
-            }
18596
+    return DayGridEventRenderer;
18597
+}(EventRenderer_1.default));
18598
+exports.default = DayGridEventRenderer;
18599
+// Computes whether two segments' columns collide. They are assumed to be in the same row.
18600
+function isDaySegCollision(seg, otherSegs) {
18601
+    var i;
18602
+    var otherSeg;
18603
+    for (i = 0; i < otherSegs.length; i++) {
18604
+        otherSeg = otherSegs[i];
18605
+        if (otherSeg.leftCol <= seg.rightCol &&
18606
+            otherSeg.rightCol >= seg.leftCol) {
18607
+            return true;
18608
         }
18609
-    };
18610
-    // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
18611
-    // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
18612
-    // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
18613
-    //
18614
-    // The segment might be part of a "series", which means consecutive segments with the same pressure
18615
-    // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
18616
-    // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
18617
-    // coordinate of the first segment in the series.
18618
-    TimeGridEventRenderer.prototype.computeFgSegForwardBack = function (seg, seriesBackwardPressure, seriesBackwardCoord) {
18619
-        var forwardSegs = seg.forwardSegs;
18620
-        var i;
18621
-        if (seg.forwardCoord === undefined) {
18622
-            if (!forwardSegs.length) {
18623
-                // if there are no forward segments, this segment should butt up against the edge
18624
-                seg.forwardCoord = 1;
18625
+    }
18626
+    return false;
18627
+}
18628
+// A cmp function for determining the leftmost event
18629
+function compareDaySegCols(a, b) {
18630
+    return a.leftCol - b.leftCol;
18631
+}
18632
+
18633
+
18634
+/***/ }),
18635
+/* 244 */
18636
+/***/ (function(module, exports, __webpack_require__) {
18637
+
18638
+Object.defineProperty(exports, "__esModule", { value: true });
18639
+var tslib_1 = __webpack_require__(2);
18640
+var $ = __webpack_require__(3);
18641
+var HelperRenderer_1 = __webpack_require__(63);
18642
+var DayGridHelperRenderer = /** @class */ (function (_super) {
18643
+    tslib_1.__extends(DayGridHelperRenderer, _super);
18644
+    function DayGridHelperRenderer() {
18645
+        return _super !== null && _super.apply(this, arguments) || this;
18646
+    }
18647
+    // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
18648
+    DayGridHelperRenderer.prototype.renderSegs = function (segs, sourceSeg) {
18649
+        var helperNodes = [];
18650
+        var rowStructs;
18651
+        // TODO: not good to call eventRenderer this way
18652
+        rowStructs = this.eventRenderer.renderSegRows(segs);
18653
+        // inject each new event skeleton into each associated row
18654
+        this.component.rowEls.each(function (row, rowNode) {
18655
+            var rowEl = $(rowNode); // the .fc-row
18656
+            var skeletonEl = $('<div class="fc-helper-skeleton"><table></table></div>'); // will be absolutely positioned
18657
+            var skeletonTopEl;
18658
+            var skeletonTop;
18659
+            // If there is an original segment, match the top position. Otherwise, put it at the row's top level
18660
+            if (sourceSeg && sourceSeg.row === row) {
18661
+                skeletonTop = sourceSeg.el.position().top;
18662
             }
18663
             else {
18664
-                // sort highest pressure first
18665
-                this.sortForwardSegs(forwardSegs);
18666
-                // this segment's forwardCoord will be calculated from the backwardCoord of the
18667
-                // highest-pressure forward segment.
18668
-                this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
18669
-                seg.forwardCoord = forwardSegs[0].backwardCoord;
18670
-            }
18671
-            // calculate the backwardCoord from the forwardCoord. consider the series
18672
-            seg.backwardCoord = seg.forwardCoord -
18673
-                (seg.forwardCoord - seriesBackwardCoord) / // available width for series
18674
-                    (seriesBackwardPressure + 1); // # of segments in the series
18675
-            // use this segment's coordinates to computed the coordinates of the less-pressurized
18676
-            // forward segments
18677
-            for (i = 0; i < forwardSegs.length; i++) {
18678
-                this.computeFgSegForwardBack(forwardSegs[i], 0, seg.forwardCoord);
18679
+                skeletonTopEl = rowEl.find('.fc-content-skeleton tbody');
18680
+                if (!skeletonTopEl.length) { // when no events
18681
+                    skeletonTopEl = rowEl.find('.fc-content-skeleton table');
18682
+                }
18683
+                skeletonTop = skeletonTopEl.position().top;
18684
             }
18685
-        }
18686
-    };
18687
-    TimeGridEventRenderer.prototype.sortForwardSegs = function (forwardSegs) {
18688
-        forwardSegs.sort(util_1.proxy(this, 'compareForwardSegs'));
18689
-    };
18690
-    // A cmp function for determining which forward segment to rely on more when computing coordinates.
18691
-    TimeGridEventRenderer.prototype.compareForwardSegs = function (seg1, seg2) {
18692
-        // put higher-pressure first
18693
-        return seg2.forwardPressure - seg1.forwardPressure ||
18694
-            // put segments that are closer to initial edge first (and favor ones with no coords yet)
18695
-            (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
18696
-            // do normal sorting...
18697
-            this.compareEventSegs(seg1, seg2);
18698
+            skeletonEl.css('top', skeletonTop)
18699
+                .find('table')
18700
+                .append(rowStructs[row].tbodyEl);
18701
+            rowEl.append(skeletonEl);
18702
+            helperNodes.push(skeletonEl[0]);
18703
+        });
18704
+        return $(helperNodes); // must return the elements rendered
18705
     };
18706
-    // Given foreground event segments that have already had their position coordinates computed,
18707
-    // assigns position-related CSS values to their elements.
18708
-    TimeGridEventRenderer.prototype.assignFgSegHorizontals = function (segs) {
18709
+    return DayGridHelperRenderer;
18710
+}(HelperRenderer_1.default));
18711
+exports.default = DayGridHelperRenderer;
18712
+
18713
+
18714
+/***/ }),
18715
+/* 245 */
18716
+/***/ (function(module, exports, __webpack_require__) {
18717
+
18718
+Object.defineProperty(exports, "__esModule", { value: true });
18719
+var tslib_1 = __webpack_require__(2);
18720
+var $ = __webpack_require__(3);
18721
+var FillRenderer_1 = __webpack_require__(62);
18722
+var DayGridFillRenderer = /** @class */ (function (_super) {
18723
+    tslib_1.__extends(DayGridFillRenderer, _super);
18724
+    function DayGridFillRenderer() {
18725
+        var _this = _super !== null && _super.apply(this, arguments) || this;
18726
+        _this.fillSegTag = 'td'; // override the default tag name
18727
+        return _this;
18728
+    }
18729
+    DayGridFillRenderer.prototype.attachSegEls = function (type, segs) {
18730
+        var nodes = [];
18731
         var i;
18732
         var seg;
18733
+        var skeletonEl;
18734
         for (i = 0; i < segs.length; i++) {
18735
             seg = segs[i];
18736
-            seg.el.css(this.generateFgSegHorizontalCss(seg));
18737
-            // if the height is short, add a className for alternate styling
18738
-            if (seg.bottom - seg.top < 30) {
18739
-                seg.el.addClass('fc-short');
18740
-            }
18741
+            skeletonEl = this.renderFillRow(type, seg);
18742
+            this.component.rowEls.eq(seg.row).append(skeletonEl);
18743
+            nodes.push(skeletonEl[0]);
18744
         }
18745
+        return nodes;
18746
     };
18747
-    // Generates an object with CSS properties/values that should be applied to an event segment element.
18748
-    // Contains important positioning-related properties that should be applied to any event element, customized or not.
18749
-    TimeGridEventRenderer.prototype.generateFgSegHorizontalCss = function (seg) {
18750
-        var shouldOverlap = this.opt('slotEventOverlap');
18751
-        var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
18752
-        var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
18753
-        var props = this.timeGrid.generateSegVerticalCss(seg); // get top/bottom first
18754
-        var isRTL = this.timeGrid.isRTL;
18755
-        var left; // amount of space from left edge, a fraction of the total width
18756
-        var right; // amount of space from right edge, a fraction of the total width
18757
-        if (shouldOverlap) {
18758
-            // double the width, but don't go beyond the maximum forward coordinate (1.0)
18759
-            forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
18760
-        }
18761
-        if (isRTL) {
18762
-            left = 1 - forwardCoord;
18763
-            right = backwardCoord;
18764
+    // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
18765
+    DayGridFillRenderer.prototype.renderFillRow = function (type, seg) {
18766
+        var colCnt = this.component.colCnt;
18767
+        var startCol = seg.leftCol;
18768
+        var endCol = seg.rightCol + 1;
18769
+        var className;
18770
+        var skeletonEl;
18771
+        var trEl;
18772
+        if (type === 'businessHours') {
18773
+            className = 'bgevent';
18774
         }
18775
         else {
18776
-            left = backwardCoord;
18777
-            right = 1 - forwardCoord;
18778
-        }
18779
-        props.zIndex = seg.level + 1; // convert from 0-base to 1-based
18780
-        props.left = left * 100 + '%';
18781
-        props.right = right * 100 + '%';
18782
-        if (shouldOverlap && seg.forwardPressure) {
18783
-            // add padding to the edge so that forward stacked events don't cover the resizer's icon
18784
-            props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
18785
-        }
18786
-        return props;
18787
-    };
18788
-    return TimeGridEventRenderer;
18789
-}(EventRenderer_1.default));
18790
-exports.default = TimeGridEventRenderer;
18791
-// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
18792
-// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
18793
-function buildSlotSegLevels(segs) {
18794
-    var levels = [];
18795
-    var i;
18796
-    var seg;
18797
-    var j;
18798
-    for (i = 0; i < segs.length; i++) {
18799
-        seg = segs[i];
18800
-        // go through all the levels and stop on the first level where there are no collisions
18801
-        for (j = 0; j < levels.length; j++) {
18802
-            if (!computeSlotSegCollisions(seg, levels[j]).length) {
18803
-                break;
18804
-            }
18805
-        }
18806
-        seg.level = j;
18807
-        (levels[j] || (levels[j] = [])).push(seg);
18808
-    }
18809
-    return levels;
18810
-}
18811
-// For every segment, figure out the other segments that are in subsequent
18812
-// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
18813
-function computeForwardSlotSegs(levels) {
18814
-    var i;
18815
-    var level;
18816
-    var j;
18817
-    var seg;
18818
-    var k;
18819
-    for (i = 0; i < levels.length; i++) {
18820
-        level = levels[i];
18821
-        for (j = 0; j < level.length; j++) {
18822
-            seg = level[j];
18823
-            seg.forwardSegs = [];
18824
-            for (k = i + 1; k < levels.length; k++) {
18825
-                computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
18826
-            }
18827
+            className = type.toLowerCase();
18828
         }
18829
-    }
18830
-}
18831
-// Figure out which path forward (via seg.forwardSegs) results in the longest path until
18832
-// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
18833
-function computeSlotSegPressures(seg) {
18834
-    var forwardSegs = seg.forwardSegs;
18835
-    var forwardPressure = 0;
18836
-    var i;
18837
-    var forwardSeg;
18838
-    if (seg.forwardPressure === undefined) {
18839
-        for (i = 0; i < forwardSegs.length; i++) {
18840
-            forwardSeg = forwardSegs[i];
18841
-            // figure out the child's maximum forward path
18842
-            computeSlotSegPressures(forwardSeg);
18843
-            // either use the existing maximum, or use the child's forward pressure
18844
-            // plus one (for the forwardSeg itself)
18845
-            forwardPressure = Math.max(forwardPressure, 1 + forwardSeg.forwardPressure);
18846
+        skeletonEl = $('<div class="fc-' + className + '-skeleton">' +
18847
+            '<table><tr></tr></table>' +
18848
+            '</div>');
18849
+        trEl = skeletonEl.find('tr');
18850
+        if (startCol > 0) {
18851
+            trEl.append(
18852
+            // will create (startCol + 1) td's
18853
+            new Array(startCol + 1).join('<td></td>'));
18854
         }
18855
-        seg.forwardPressure = forwardPressure;
18856
-    }
18857
-}
18858
-// Find all the segments in `otherSegs` that vertically collide with `seg`.
18859
-// Append into an optionally-supplied `results` array and return.
18860
-function computeSlotSegCollisions(seg, otherSegs, results) {
18861
-    if (results === void 0) { results = []; }
18862
-    for (var i = 0; i < otherSegs.length; i++) {
18863
-        if (isSlotSegCollision(seg, otherSegs[i])) {
18864
-            results.push(otherSegs[i]);
18865
+        trEl.append(seg.el.attr('colspan', endCol - startCol));
18866
+        if (endCol < colCnt) {
18867
+            trEl.append(
18868
+            // will create (colCnt - endCol) td's
18869
+            new Array(colCnt - endCol + 1).join('<td></td>'));
18870
         }
18871
-    }
18872
-    return results;
18873
-}
18874
-// Do these segments occupy the same vertical space?
18875
-function isSlotSegCollision(seg1, seg2) {
18876
-    return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
18877
-}
18878
+        this.component.bookendCells(trEl);
18879
+        return skeletonEl;
18880
+    };
18881
+    return DayGridFillRenderer;
18882
+}(FillRenderer_1.default));
18883
+exports.default = DayGridFillRenderer;
18884
 
18885
 
18886
 /***/ }),
18887
-/* 247 */
18888
+/* 246 */
18889
 /***/ (function(module, exports, __webpack_require__) {
18890
 
18891
 Object.defineProperty(exports, "__esModule", { value: true });
18892
 var tslib_1 = __webpack_require__(2);
18893
-var $ = __webpack_require__(3);
18894
-var HelperRenderer_1 = __webpack_require__(58);
18895
-var TimeGridHelperRenderer = /** @class */ (function (_super) {
18896
-    tslib_1.__extends(TimeGridHelperRenderer, _super);
18897
-    function TimeGridHelperRenderer() {
18898
+var moment = __webpack_require__(0);
18899
+var util_1 = __webpack_require__(4);
18900
+var BasicView_1 = __webpack_require__(67);
18901
+var MonthViewDateProfileGenerator_1 = __webpack_require__(247);
18902
+/* A month view with day cells running in rows (one-per-week) and columns
18903
+----------------------------------------------------------------------------------------------------------------------*/
18904
+var MonthView = /** @class */ (function (_super) {
18905
+    tslib_1.__extends(MonthView, _super);
18906
+    function MonthView() {
18907
         return _super !== null && _super.apply(this, arguments) || this;
18908
     }
18909
-    TimeGridHelperRenderer.prototype.renderSegs = function (segs, sourceSeg) {
18910
-        var helperNodes = [];
18911
-        var i;
18912
-        var seg;
18913
-        var sourceEl;
18914
-        // TODO: not good to call eventRenderer this way
18915
-        this.eventRenderer.renderFgSegsIntoContainers(segs, this.component.helperContainerEls);
18916
-        // Try to make the segment that is in the same row as sourceSeg look the same
18917
-        for (i = 0; i < segs.length; i++) {
18918
-            seg = segs[i];
18919
-            if (sourceSeg && sourceSeg.col === seg.col) {
18920
-                sourceEl = sourceSeg.el;
18921
-                seg.el.css({
18922
-                    left: sourceEl.css('left'),
18923
-                    right: sourceEl.css('right'),
18924
-                    'margin-left': sourceEl.css('margin-left'),
18925
-                    'margin-right': sourceEl.css('margin-right')
18926
-                });
18927
-            }
18928
-            helperNodes.push(seg.el[0]);
18929
+    // Overrides the default BasicView behavior to have special multi-week auto-height logic
18930
+    MonthView.prototype.setGridHeight = function (height, isAuto) {
18931
+        // if auto, make the height of each row the height that it would be if there were 6 weeks
18932
+        if (isAuto) {
18933
+            height *= this.dayGrid.rowCnt / 6;
18934
         }
18935
-        return $(helperNodes); // must return the elements rendered
18936
+        util_1.distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
18937
     };
18938
-    return TimeGridHelperRenderer;
18939
-}(HelperRenderer_1.default));
18940
-exports.default = TimeGridHelperRenderer;
18941
+    MonthView.prototype.isDateInOtherMonth = function (date, dateProfile) {
18942
+        return date.month() !== moment.utc(dateProfile.currentUnzonedRange.startMs).month(); // TODO: optimize
18943
+    };
18944
+    return MonthView;
18945
+}(BasicView_1.default));
18946
+exports.default = MonthView;
18947
+MonthView.prototype.dateProfileGeneratorClass = MonthViewDateProfileGenerator_1.default;
18948
 
18949
 
18950
 /***/ }),
18951
-/* 248 */
18952
+/* 247 */
18953
 /***/ (function(module, exports, __webpack_require__) {
18954
 
18955
 Object.defineProperty(exports, "__esModule", { value: true });
18956
 var tslib_1 = __webpack_require__(2);
18957
-var FillRenderer_1 = __webpack_require__(57);
18958
-var TimeGridFillRenderer = /** @class */ (function (_super) {
18959
-    tslib_1.__extends(TimeGridFillRenderer, _super);
18960
-    function TimeGridFillRenderer() {
18961
+var BasicViewDateProfileGenerator_1 = __webpack_require__(68);
18962
+var UnzonedRange_1 = __webpack_require__(5);
18963
+var MonthViewDateProfileGenerator = /** @class */ (function (_super) {
18964
+    tslib_1.__extends(MonthViewDateProfileGenerator, _super);
18965
+    function MonthViewDateProfileGenerator() {
18966
         return _super !== null && _super.apply(this, arguments) || this;
18967
     }
18968
-    TimeGridFillRenderer.prototype.attachSegEls = function (type, segs) {
18969
-        var timeGrid = this.component;
18970
-        var containerEls;
18971
-        // TODO: more efficient lookup
18972
-        if (type === 'bgEvent') {
18973
-            containerEls = timeGrid.bgContainerEls;
18974
-        }
18975
-        else if (type === 'businessHours') {
18976
-            containerEls = timeGrid.businessContainerEls;
18977
-        }
18978
-        else if (type === 'highlight') {
18979
-            containerEls = timeGrid.highlightContainerEls;
18980
+    // Computes the date range that will be rendered.
18981
+    MonthViewDateProfileGenerator.prototype.buildRenderRange = function (currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
18982
+        var renderUnzonedRange = _super.prototype.buildRenderRange.call(this, currentUnzonedRange, currentRangeUnit, isRangeAllDay);
18983
+        var start = this.msToUtcMoment(renderUnzonedRange.startMs, isRangeAllDay);
18984
+        var end = this.msToUtcMoment(renderUnzonedRange.endMs, isRangeAllDay);
18985
+        var rowCnt;
18986
+        // ensure 6 weeks
18987
+        if (this.opt('fixedWeekCount')) {
18988
+            rowCnt = Math.ceil(// could be partial weeks due to hiddenDays
18989
+            end.diff(start, 'weeks', true) // dontRound=true
18990
+            );
18991
+            end.add(6 - rowCnt, 'weeks');
18992
         }
18993
-        timeGrid.updateSegVerticals(segs);
18994
-        timeGrid.attachSegsByCol(timeGrid.groupSegsByCol(segs), containerEls);
18995
-        return segs.map(function (seg) {
18996
-            return seg.el[0];
18997
-        });
18998
+        return new UnzonedRange_1.default(start, end);
18999
     };
19000
-    return TimeGridFillRenderer;
19001
-}(FillRenderer_1.default));
19002
-exports.default = TimeGridFillRenderer;
19003
+    return MonthViewDateProfileGenerator;
19004
+}(BasicViewDateProfileGenerator_1.default));
19005
+exports.default = MonthViewDateProfileGenerator;
19006
 
19007
 
19008
 /***/ }),
19009
-/* 249 */
19010
+/* 248 */
19011
 /***/ (function(module, exports, __webpack_require__) {
19012
 
19013
-/* A rectangular panel that is absolutely positioned over other content
19014
-------------------------------------------------------------------------------------------------------------------------
19015
-Options:
19016
-  - className (string)
19017
-  - content (HTML string or jQuery element set)
19018
-  - parentEl
19019
-  - top
19020
-  - left
19021
-  - right (the x coord of where the right edge should be. not a "CSS" right)
19022
-  - autoHide (boolean)
19023
-  - show (callback)
19024
-  - hide (callback)
19025
-*/
19026
 Object.defineProperty(exports, "__esModule", { value: true });
19027
+var tslib_1 = __webpack_require__(2);
19028
 var $ = __webpack_require__(3);
19029
 var util_1 = __webpack_require__(4);
19030
-var ListenerMixin_1 = __webpack_require__(7);
19031
-var Popover = /** @class */ (function () {
19032
-    function Popover(options) {
19033
-        this.isHidden = true;
19034
-        this.margin = 10; // the space required between the popover and the edges of the scroll container
19035
-        this.options = options || {};
19036
+var UnzonedRange_1 = __webpack_require__(5);
19037
+var View_1 = __webpack_require__(43);
19038
+var Scroller_1 = __webpack_require__(41);
19039
+var ListEventRenderer_1 = __webpack_require__(249);
19040
+var ListEventPointing_1 = __webpack_require__(250);
19041
+/*
19042
+Responsible for the scroller, and forwarding event-related actions into the "grid".
19043
+*/
19044
+var ListView = /** @class */ (function (_super) {
19045
+    tslib_1.__extends(ListView, _super);
19046
+    function ListView(calendar, viewSpec) {
19047
+        var _this = _super.call(this, calendar, viewSpec) || this;
19048
+        _this.segSelector = '.fc-list-item'; // which elements accept event actions
19049
+        _this.scroller = new Scroller_1.default({
19050
+            overflowX: 'hidden',
19051
+            overflowY: 'auto'
19052
+        });
19053
+        return _this;
19054
     }
19055
-    // Shows the popover on the specified position. Renders it if not already
19056
-    Popover.prototype.show = function () {
19057
-        if (this.isHidden) {
19058
-            if (!this.el) {
19059
-                this.render();
19060
+    ListView.prototype.renderSkeleton = function () {
19061
+        this.el.addClass('fc-list-view ' +
19062
+            this.calendar.theme.getClass('listView'));
19063
+        this.scroller.render();
19064
+        this.scroller.el.appendTo(this.el);
19065
+        this.contentEl = this.scroller.scrollEl; // shortcut
19066
+    };
19067
+    ListView.prototype.unrenderSkeleton = function () {
19068
+        this.scroller.destroy(); // will remove the Grid too
19069
+    };
19070
+    ListView.prototype.updateSize = function (totalHeight, isAuto, isResize) {
19071
+        _super.prototype.updateSize.call(this, totalHeight, isAuto, isResize);
19072
+        this.scroller.clear(); // sets height to 'auto' and clears overflow
19073
+        if (!isAuto) {
19074
+            this.scroller.setHeight(this.computeScrollerHeight(totalHeight));
19075
+        }
19076
+    };
19077
+    ListView.prototype.computeScrollerHeight = function (totalHeight) {
19078
+        return totalHeight -
19079
+            util_1.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
19080
+    };
19081
+    ListView.prototype.renderDates = function (dateProfile) {
19082
+        var calendar = this.calendar;
19083
+        var dayStart = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.startMs, true);
19084
+        var viewEnd = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.endMs, true);
19085
+        var dayDates = [];
19086
+        var dayRanges = [];
19087
+        while (dayStart < viewEnd) {
19088
+            dayDates.push(dayStart.clone());
19089
+            dayRanges.push(new UnzonedRange_1.default(dayStart, dayStart.clone().add(1, 'day')));
19090
+            dayStart.add(1, 'day');
19091
+        }
19092
+        this.dayDates = dayDates;
19093
+        this.dayRanges = dayRanges;
19094
+        // all real rendering happens in EventRenderer
19095
+    };
19096
+    // slices by day
19097
+    ListView.prototype.componentFootprintToSegs = function (footprint) {
19098
+        var dayRanges = this.dayRanges;
19099
+        var dayIndex;
19100
+        var segRange;
19101
+        var seg;
19102
+        var segs = [];
19103
+        for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex++) {
19104
+            segRange = footprint.unzonedRange.intersect(dayRanges[dayIndex]);
19105
+            if (segRange) {
19106
+                seg = {
19107
+                    startMs: segRange.startMs,
19108
+                    endMs: segRange.endMs,
19109
+                    isStart: segRange.isStart,
19110
+                    isEnd: segRange.isEnd,
19111
+                    dayIndex: dayIndex
19112
+                };
19113
+                segs.push(seg);
19114
+                // detect when footprint won't go fully into the next day,
19115
+                // and mutate the latest seg to the be the end.
19116
+                if (!seg.isEnd && !footprint.isAllDay &&
19117
+                    dayIndex + 1 < dayRanges.length &&
19118
+                    footprint.unzonedRange.endMs < dayRanges[dayIndex + 1].startMs + this.nextDayThreshold) {
19119
+                    seg.endMs = footprint.unzonedRange.endMs;
19120
+                    seg.isEnd = true;
19121
+                    break;
19122
+                }
19123
             }
19124
-            this.el.show();
19125
-            this.position();
19126
-            this.isHidden = false;
19127
-            this.trigger('show');
19128
         }
19129
+        return segs;
19130
     };
19131
-    // Hides the popover, through CSS, but does not remove it from the DOM
19132
-    Popover.prototype.hide = function () {
19133
-        if (!this.isHidden) {
19134
-            this.el.hide();
19135
-            this.isHidden = true;
19136
-            this.trigger('hide');
19137
+    ListView.prototype.renderEmptyMessage = function () {
19138
+        this.contentEl.html('<div class="fc-list-empty-wrap2">' + // TODO: try less wraps
19139
+            '<div class="fc-list-empty-wrap1">' +
19140
+            '<div class="fc-list-empty">' +
19141
+            util_1.htmlEscape(this.opt('noEventsMessage')) +
19142
+            '</div>' +
19143
+            '</div>' +
19144
+            '</div>');
19145
+    };
19146
+    // render the event segments in the view
19147
+    ListView.prototype.renderSegList = function (allSegs) {
19148
+        var segsByDay = this.groupSegsByDay(allSegs); // sparse array
19149
+        var dayIndex;
19150
+        var daySegs;
19151
+        var i;
19152
+        var tableEl = $('<table class="fc-list-table ' + this.calendar.theme.getClass('tableList') + '"><tbody></tbody></table>');
19153
+        var tbodyEl = tableEl.find('tbody');
19154
+        for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) {
19155
+            daySegs = segsByDay[dayIndex];
19156
+            if (daySegs) { // sparse array, so might be undefined
19157
+                // append a day header
19158
+                tbodyEl.append(this.dayHeaderHtml(this.dayDates[dayIndex]));
19159
+                this.eventRenderer.sortEventSegs(daySegs);
19160
+                for (i = 0; i < daySegs.length; i++) {
19161
+                    tbodyEl.append(daySegs[i].el); // append event row
19162
+                }
19163
+            }
19164
         }
19165
+        this.contentEl.empty().append(tableEl);
19166
     };
19167
-    // Creates `this.el` and renders content inside of it
19168
-    Popover.prototype.render = function () {
19169
-        var _this = this;
19170
-        var options = this.options;
19171
-        this.el = $('<div class="fc-popover"/>')
19172
-            .addClass(options.className || '')
19173
-            .css({
19174
-            // position initially to the top left to avoid creating scrollbars
19175
-            top: 0,
19176
-            left: 0
19177
-        })
19178
-            .append(options.content)
19179
-            .appendTo(options.parentEl);
19180
-        // when a click happens on anything inside with a 'fc-close' className, hide the popover
19181
-        this.el.on('click', '.fc-close', function () {
19182
-            _this.hide();
19183
-        });
19184
-        if (options.autoHide) {
19185
-            this.listenTo($(document), 'mousedown', this.documentMousedown);
19186
+    // Returns a sparse array of arrays, segs grouped by their dayIndex
19187
+    ListView.prototype.groupSegsByDay = function (segs) {
19188
+        var segsByDay = []; // sparse array
19189
+        var i;
19190
+        var seg;
19191
+        for (i = 0; i < segs.length; i++) {
19192
+            seg = segs[i];
19193
+            (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = []))
19194
+                .push(seg);
19195
         }
19196
+        return segsByDay;
19197
     };
19198
-    // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
19199
-    Popover.prototype.documentMousedown = function (ev) {
19200
-        // only hide the popover if the click happened outside the popover
19201
-        if (this.el && !$(ev.target).closest(this.el).length) {
19202
-            this.hide();
19203
+    // generates the HTML for the day headers that live amongst the event rows
19204
+    ListView.prototype.dayHeaderHtml = function (dayDate) {
19205
+        var mainFormat = this.opt('listDayFormat');
19206
+        var altFormat = this.opt('listDayAltFormat');
19207
+        return '<tr class="fc-list-heading" data-date="' + dayDate.format('YYYY-MM-DD') + '">' +
19208
+            '<td class="' + (this.calendar.theme.getClass('tableListHeading') ||
19209
+            this.calendar.theme.getClass('widgetHeader')) + '" colspan="3">' +
19210
+            (mainFormat ?
19211
+                this.buildGotoAnchorHtml(dayDate, { 'class': 'fc-list-heading-main' }, util_1.htmlEscape(dayDate.format(mainFormat)) // inner HTML
19212
+                ) :
19213
+                '') +
19214
+            (altFormat ?
19215
+                this.buildGotoAnchorHtml(dayDate, { 'class': 'fc-list-heading-alt' }, util_1.htmlEscape(dayDate.format(altFormat)) // inner HTML
19216
+                ) :
19217
+                '') +
19218
+            '</td>' +
19219
+            '</tr>';
19220
+    };
19221
+    return ListView;
19222
+}(View_1.default));
19223
+exports.default = ListView;
19224
+ListView.prototype.eventRendererClass = ListEventRenderer_1.default;
19225
+ListView.prototype.eventPointingClass = ListEventPointing_1.default;
19226
+
19227
+
19228
+/***/ }),
19229
+/* 249 */
19230
+/***/ (function(module, exports, __webpack_require__) {
19231
+
19232
+Object.defineProperty(exports, "__esModule", { value: true });
19233
+var tslib_1 = __webpack_require__(2);
19234
+var util_1 = __webpack_require__(4);
19235
+var EventRenderer_1 = __webpack_require__(44);
19236
+var ListEventRenderer = /** @class */ (function (_super) {
19237
+    tslib_1.__extends(ListEventRenderer, _super);
19238
+    function ListEventRenderer() {
19239
+        return _super !== null && _super.apply(this, arguments) || this;
19240
+    }
19241
+    ListEventRenderer.prototype.renderFgSegs = function (segs) {
19242
+        if (!segs.length) {
19243
+            this.component.renderEmptyMessage();
19244
         }
19245
-    };
19246
-    // Hides and unregisters any handlers
19247
-    Popover.prototype.removeElement = function () {
19248
-        this.hide();
19249
-        if (this.el) {
19250
-            this.el.remove();
19251
-            this.el = null;
19252
+        else {
19253
+            this.component.renderSegList(segs);
19254
         }
19255
-        this.stopListeningTo($(document), 'mousedown');
19256
     };
19257
-    // Positions the popover optimally, using the top/left/right options
19258
-    Popover.prototype.position = function () {
19259
-        var options = this.options;
19260
-        var origin = this.el.offsetParent().offset();
19261
-        var width = this.el.outerWidth();
19262
-        var height = this.el.outerHeight();
19263
-        var windowEl = $(window);
19264
-        var viewportEl = util_1.getScrollParent(this.el);
19265
-        var viewportTop;
19266
-        var viewportLeft;
19267
-        var viewportOffset;
19268
-        var top; // the "position" (not "offset") values for the popover
19269
-        var left; //
19270
-        // compute top and left
19271
-        top = options.top || 0;
19272
-        if (options.left !== undefined) {
19273
-            left = options.left;
19274
-        }
19275
-        else if (options.right !== undefined) {
19276
-            left = options.right - width; // derive the left value from the right value
19277
-        }
19278
-        else {
19279
-            left = 0;
19280
+    // generates the HTML for a single event row
19281
+    ListEventRenderer.prototype.fgSegHtml = function (seg) {
19282
+        var view = this.view;
19283
+        var calendar = view.calendar;
19284
+        var theme = calendar.theme;
19285
+        var eventFootprint = seg.footprint;
19286
+        var eventDef = eventFootprint.eventDef;
19287
+        var componentFootprint = eventFootprint.componentFootprint;
19288
+        var url = eventDef.url;
19289
+        var classes = ['fc-list-item'].concat(this.getClasses(eventDef));
19290
+        var bgColor = this.getBgColor(eventDef);
19291
+        var timeHtml;
19292
+        if (componentFootprint.isAllDay) {
19293
+            timeHtml = view.getAllDayHtml();
19294
         }
19295
-        if (viewportEl.is(window) || viewportEl.is(document)) {
19296
-            viewportEl = windowEl;
19297
-            viewportTop = 0; // the window is always at the top left
19298
-            viewportLeft = 0; // (and .offset() won't work if called here)
19299
+        else if (view.isMultiDayRange(componentFootprint.unzonedRange)) {
19300
+            if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day
19301
+                timeHtml = util_1.htmlEscape(this._getTimeText(calendar.msToMoment(seg.startMs), calendar.msToMoment(seg.endMs), componentFootprint.isAllDay));
19302
+            }
19303
+            else { // inner segment that lasts the whole day
19304
+                timeHtml = view.getAllDayHtml();
19305
+            }
19306
         }
19307
         else {
19308
-            viewportOffset = viewportEl.offset();
19309
-            viewportTop = viewportOffset.top;
19310
-            viewportLeft = viewportOffset.left;
19311
+            // Display the normal time text for the *event's* times
19312
+            timeHtml = util_1.htmlEscape(this.getTimeText(eventFootprint));
19313
         }
19314
-        // if the window is scrolled, it causes the visible area to be further down
19315
-        viewportTop += windowEl.scrollTop();
19316
-        viewportLeft += windowEl.scrollLeft();
19317
-        // constrain to the view port. if constrained by two edges, give precedence to top/left
19318
-        if (options.viewportConstrain !== false) {
19319
-            top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
19320
-            top = Math.max(top, viewportTop + this.margin);
19321
-            left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
19322
-            left = Math.max(left, viewportLeft + this.margin);
19323
+        if (url) {
19324
+            classes.push('fc-has-url');
19325
         }
19326
-        this.el.css({
19327
-            top: top - origin.top,
19328
-            left: left - origin.left
19329
-        });
19330
+        return '<tr class="' + classes.join(' ') + '">' +
19331
+            (this.displayEventTime ?
19332
+                '<td class="fc-list-item-time ' + theme.getClass('widgetContent') + '">' +
19333
+                    (timeHtml || '') +
19334
+                    '</td>' :
19335
+                '') +
19336
+            '<td class="fc-list-item-marker ' + theme.getClass('widgetContent') + '">' +
19337
+            '<span class="fc-event-dot"' +
19338
+            (bgColor ?
19339
+                ' style="background-color:' + bgColor + '"' :
19340
+                '') +
19341
+            '></span>' +
19342
+            '</td>' +
19343
+            '<td class="fc-list-item-title ' + theme.getClass('widgetContent') + '">' +
19344
+            '<a' + (url ? ' href="' + util_1.htmlEscape(url) + '"' : '') + '>' +
19345
+            util_1.htmlEscape(eventDef.title || '') +
19346
+            '</a>' +
19347
+            '</td>' +
19348
+            '</tr>';
19349
     };
19350
-    // Triggers a callback. Calls a function in the option hash of the same name.
19351
-    // Arguments beyond the first `name` are forwarded on.
19352
-    // TODO: better code reuse for this. Repeat code
19353
-    Popover.prototype.trigger = function (name) {
19354
-        if (this.options[name]) {
19355
-            this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
19356
-        }
19357
+    // like "4:00am"
19358
+    ListEventRenderer.prototype.computeEventTimeFormat = function () {
19359
+        return this.opt('mediumTimeFormat');
19360
     };
19361
-    return Popover;
19362
-}());
19363
-exports.default = Popover;
19364
-ListenerMixin_1.default.mixInto(Popover);
19365
+    return ListEventRenderer;
19366
+}(EventRenderer_1.default));
19367
+exports.default = ListEventRenderer;
19368
 
19369
 
19370
 /***/ }),
19371
@@ -14270,530 +14336,602 @@
19372
 Object.defineProperty(exports, "__esModule", { value: true });
19373
 var tslib_1 = __webpack_require__(2);
19374
 var $ = __webpack_require__(3);
19375
-var util_1 = __webpack_require__(4);
19376
-var EventRenderer_1 = __webpack_require__(42);
19377
-/* Event-rendering methods for the DayGrid class
19378
-----------------------------------------------------------------------------------------------------------------------*/
19379
-var DayGridEventRenderer = /** @class */ (function (_super) {
19380
-    tslib_1.__extends(DayGridEventRenderer, _super);
19381
-    function DayGridEventRenderer(dayGrid, fillRenderer) {
19382
-        var _this = _super.call(this, dayGrid, fillRenderer) || this;
19383
-        _this.dayGrid = dayGrid;
19384
-        return _this;
19385
+var EventPointing_1 = __webpack_require__(64);
19386
+var ListEventPointing = /** @class */ (function (_super) {
19387
+    tslib_1.__extends(ListEventPointing, _super);
19388
+    function ListEventPointing() {
19389
+        return _super !== null && _super.apply(this, arguments) || this;
19390
     }
19391
-    DayGridEventRenderer.prototype.renderBgRanges = function (eventRanges) {
19392
-        // don't render timed background events
19393
-        eventRanges = $.grep(eventRanges, function (eventRange) {
19394
-            return eventRange.eventDef.isAllDay();
19395
-        });
19396
-        _super.prototype.renderBgRanges.call(this, eventRanges);
19397
-    };
19398
-    // Renders the given foreground event segments onto the grid
19399
-    DayGridEventRenderer.prototype.renderFgSegs = function (segs) {
19400
-        var rowStructs = this.rowStructs = this.renderSegRows(segs);
19401
-        // append to each row's content skeleton
19402
-        this.dayGrid.rowEls.each(function (i, rowNode) {
19403
-            $(rowNode).find('.fc-content-skeleton > table').append(rowStructs[i].tbodyEl);
19404
-        });
19405
-    };
19406
-    // Unrenders all currently rendered foreground event segments
19407
-    DayGridEventRenderer.prototype.unrenderFgSegs = function () {
19408
-        var rowStructs = this.rowStructs || [];
19409
-        var rowStruct;
19410
-        while ((rowStruct = rowStructs.pop())) {
19411
-            rowStruct.tbodyEl.remove();
19412
-        }
19413
-        this.rowStructs = null;
19414
-    };
19415
-    // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
19416
-    // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
19417
-    // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
19418
-    DayGridEventRenderer.prototype.renderSegRows = function (segs) {
19419
-        var rowStructs = [];
19420
-        var segRows;
19421
-        var row;
19422
-        segRows = this.groupSegRows(segs); // group into nested arrays
19423
-        // iterate each row of segment groupings
19424
-        for (row = 0; row < segRows.length; row++) {
19425
-            rowStructs.push(this.renderSegRow(row, segRows[row]));
19426
+    // for events with a url, the whole <tr> should be clickable,
19427
+    // but it's impossible to wrap with an <a> tag. simulate this.
19428
+    ListEventPointing.prototype.handleClick = function (seg, ev) {
19429
+        var url;
19430
+        _super.prototype.handleClick.call(this, seg, ev); // might prevent the default action
19431
+        // not clicking on or within an <a> with an href
19432
+        if (!$(ev.target).closest('a[href]').length) {
19433
+            url = seg.footprint.eventDef.url;
19434
+            if (url && !ev.isDefaultPrevented()) { // jsEvent not cancelled in handler
19435
+                window.location.href = url; // simulate link click
19436
+            }
19437
         }
19438
-        return rowStructs;
19439
     };
19440
-    // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
19441
-    // the segments. Returns object with a bunch of internal data about how the render was calculated.
19442
-    // NOTE: modifies rowSegs
19443
-    DayGridEventRenderer.prototype.renderSegRow = function (row, rowSegs) {
19444
-        var colCnt = this.dayGrid.colCnt;
19445
-        var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
19446
-        var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
19447
-        var tbody = $('<tbody/>');
19448
-        var segMatrix = []; // lookup for which segments are rendered into which level+col cells
19449
-        var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
19450
-        var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
19451
-        var i;
19452
-        var levelSegs;
19453
-        var col;
19454
-        var tr;
19455
-        var j;
19456
-        var seg;
19457
-        var td;
19458
-        // populates empty cells from the current column (`col`) to `endCol`
19459
-        function emptyCellsUntil(endCol) {
19460
-            while (col < endCol) {
19461
-                // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
19462
-                td = (loneCellMatrix[i - 1] || [])[col];
19463
-                if (td) {
19464
-                    td.attr('rowspan', parseInt(td.attr('rowspan') || 1, 10) + 1);
19465
-                }
19466
-                else {
19467
-                    td = $('<td/>');
19468
-                    tr.append(td);
19469
+    return ListEventPointing;
19470
+}(EventPointing_1.default));
19471
+exports.default = ListEventPointing;
19472
+
19473
+
19474
+/***/ }),
19475
+/* 251 */,
19476
+/* 252 */,
19477
+/* 253 */,
19478
+/* 254 */,
19479
+/* 255 */,
19480
+/* 256 */
19481
+/***/ (function(module, exports, __webpack_require__) {
19482
+
19483
+var $ = __webpack_require__(3);
19484
+var exportHooks = __webpack_require__(18);
19485
+var util_1 = __webpack_require__(4);
19486
+var Calendar_1 = __webpack_require__(232);
19487
+// for intentional side-effects
19488
+__webpack_require__(11);
19489
+__webpack_require__(49);
19490
+__webpack_require__(260);
19491
+__webpack_require__(261);
19492
+__webpack_require__(264);
19493
+__webpack_require__(265);
19494
+__webpack_require__(266);
19495
+__webpack_require__(267);
19496
+$.fullCalendar = exportHooks;
19497
+$.fn.fullCalendar = function (options) {
19498
+    var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
19499
+    var res = this; // what this function will return (this jQuery object by default)
19500
+    this.each(function (i, _element) {
19501
+        var element = $(_element);
19502
+        var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
19503
+        var singleRes; // the returned value of this single method call
19504
+        // a method call
19505
+        if (typeof options === 'string') {
19506
+            if (options === 'getCalendar') {
19507
+                if (!i) { // first element only
19508
+                    res = calendar;
19509
                 }
19510
-                cellMatrix[i][col] = td;
19511
-                loneCellMatrix[i][col] = td;
19512
-                col++;
19513
             }
19514
-        }
19515
-        for (i = 0; i < levelCnt; i++) {
19516
-            levelSegs = segLevels[i];
19517
-            col = 0;
19518
-            tr = $('<tr/>');
19519
-            segMatrix.push([]);
19520
-            cellMatrix.push([]);
19521
-            loneCellMatrix.push([]);
19522
-            // levelCnt might be 1 even though there are no actual levels. protect against this.
19523
-            // this single empty row is useful for styling.
19524
-            if (levelSegs) {
19525
-                for (j = 0; j < levelSegs.length; j++) {
19526
-                    seg = levelSegs[j];
19527
-                    emptyCellsUntil(seg.leftCol);
19528
-                    // create a container that occupies or more columns. append the event element.
19529
-                    td = $('<td class="fc-event-container"/>').append(seg.el);
19530
-                    if (seg.leftCol !== seg.rightCol) {
19531
-                        td.attr('colspan', seg.rightCol - seg.leftCol + 1);
19532
-                    }
19533
-                    else {
19534
-                        loneCellMatrix[i][col] = td;
19535
-                    }
19536
-                    while (col <= seg.rightCol) {
19537
-                        cellMatrix[i][col] = td;
19538
-                        segMatrix[i][col] = seg;
19539
-                        col++;
19540
-                    }
19541
-                    tr.append(td);
19542
+            else if (options === 'destroy') { // don't warn if no calendar object
19543
+                if (calendar) {
19544
+                    calendar.destroy();
19545
+                    element.removeData('fullCalendar');
19546
                 }
19547
             }
19548
-            emptyCellsUntil(colCnt); // finish off the row
19549
-            this.dayGrid.bookendCells(tr);
19550
-            tbody.append(tr);
19551
-        }
19552
-        return {
19553
-            row: row,
19554
-            tbodyEl: tbody,
19555
-            cellMatrix: cellMatrix,
19556
-            segMatrix: segMatrix,
19557
-            segLevels: segLevels,
19558
-            segs: rowSegs
19559
-        };
19560
-    };
19561
-    // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
19562
-    // NOTE: modifies segs
19563
-    DayGridEventRenderer.prototype.buildSegLevels = function (segs) {
19564
-        var levels = [];
19565
-        var i;
19566
-        var seg;
19567
-        var j;
19568
-        // Give preference to elements with certain criteria, so they have
19569
-        // a chance to be closer to the top.
19570
-        this.sortEventSegs(segs);
19571
-        for (i = 0; i < segs.length; i++) {
19572
-            seg = segs[i];
19573
-            // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
19574
-            for (j = 0; j < levels.length; j++) {
19575
-                if (!isDaySegCollision(seg, levels[j])) {
19576
-                    break;
19577
+            else if (!calendar) {
19578
+                util_1.warn('Attempting to call a FullCalendar method on an element with no calendar.');
19579
+            }
19580
+            else if ($.isFunction(calendar[options])) {
19581
+                singleRes = calendar[options].apply(calendar, args);
19582
+                if (!i) {
19583
+                    res = singleRes; // record the first method call result
19584
+                }
19585
+                if (options === 'destroy') { // for the destroy method, must remove Calendar object data
19586
+                    element.removeData('fullCalendar');
19587
                 }
19588
             }
19589
-            // `j` now holds the desired subrow index
19590
-            seg.level = j;
19591
-            // create new level array if needed and append segment
19592
-            (levels[j] || (levels[j] = [])).push(seg);
19593
+            else {
19594
+                util_1.warn("'" + options + "' is an unknown FullCalendar method.");
19595
+            }
19596
         }
19597
-        // order segments left-to-right. very important if calendar is RTL
19598
-        for (j = 0; j < levels.length; j++) {
19599
-            levels[j].sort(compareDaySegCols);
19600
+        else if (!calendar) { // don't initialize twice
19601
+            calendar = new Calendar_1.default(element, options);
19602
+            element.data('fullCalendar', calendar);
19603
+            calendar.render();
19604
         }
19605
-        return levels;
19606
+    });
19607
+    return res;
19608
+};
19609
+module.exports = exportHooks;
19610
+
19611
+
19612
+/***/ }),
19613
+/* 257 */
19614
+/***/ (function(module, exports, __webpack_require__) {
19615
+
19616
+Object.defineProperty(exports, "__esModule", { value: true });
19617
+var $ = __webpack_require__(3);
19618
+var util_1 = __webpack_require__(4);
19619
+/* Toolbar with buttons and title
19620
+----------------------------------------------------------------------------------------------------------------------*/
19621
+var Toolbar = /** @class */ (function () {
19622
+    function Toolbar(calendar, toolbarOptions) {
19623
+        this.el = null; // mirrors local `el`
19624
+        this.viewsWithButtons = [];
19625
+        this.calendar = calendar;
19626
+        this.toolbarOptions = toolbarOptions;
19627
+    }
19628
+    // method to update toolbar-specific options, not calendar-wide options
19629
+    Toolbar.prototype.setToolbarOptions = function (newToolbarOptions) {
19630
+        this.toolbarOptions = newToolbarOptions;
19631
     };
19632
-    // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
19633
-    DayGridEventRenderer.prototype.groupSegRows = function (segs) {
19634
-        var segRows = [];
19635
-        var i;
19636
-        for (i = 0; i < this.dayGrid.rowCnt; i++) {
19637
-            segRows.push([]);
19638
+    // can be called repeatedly and will rerender
19639
+    Toolbar.prototype.render = function () {
19640
+        var sections = this.toolbarOptions.layout;
19641
+        var el = this.el;
19642
+        if (sections) {
19643
+            if (!el) {
19644
+                el = this.el = $("<div class='fc-toolbar " + this.toolbarOptions.extraClasses + "'>");
19645
+            }
19646
+            else {
19647
+                el.empty();
19648
+            }
19649
+            el.append(this.renderSection('left'))
19650
+                .append(this.renderSection('right'))
19651
+                .append(this.renderSection('center'))
19652
+                .append('<div class="fc-clear"></div>');
19653
         }
19654
-        for (i = 0; i < segs.length; i++) {
19655
-            segRows[segs[i].row].push(segs[i]);
19656
+        else {
19657
+            this.removeElement();
19658
         }
19659
-        return segRows;
19660
-    };
19661
-    // Computes a default event time formatting string if `timeFormat` is not explicitly defined
19662
-    DayGridEventRenderer.prototype.computeEventTimeFormat = function () {
19663
-        return this.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
19664
     };
19665
-    // Computes a default `displayEventEnd` value if one is not expliclty defined
19666
-    DayGridEventRenderer.prototype.computeDisplayEventEnd = function () {
19667
-        return this.dayGrid.colCnt === 1; // we'll likely have space if there's only one day
19668
+    Toolbar.prototype.removeElement = function () {
19669
+        if (this.el) {
19670
+            this.el.remove();
19671
+            this.el = null;
19672
+        }
19673
     };
19674
-    // Builds the HTML to be used for the default element for an individual segment
19675
-    DayGridEventRenderer.prototype.fgSegHtml = function (seg, disableResizing) {
19676
-        var view = this.view;
19677
-        var eventDef = seg.footprint.eventDef;
19678
-        var isAllDay = seg.footprint.componentFootprint.isAllDay;
19679
-        var isDraggable = view.isEventDefDraggable(eventDef);
19680
-        var isResizableFromStart = !disableResizing && isAllDay &&
19681
-            seg.isStart && view.isEventDefResizableFromStart(eventDef);
19682
-        var isResizableFromEnd = !disableResizing && isAllDay &&
19683
-            seg.isEnd && view.isEventDefResizableFromEnd(eventDef);
19684
-        var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
19685
-        var skinCss = util_1.cssToStr(this.getSkinCss(eventDef));
19686
-        var timeHtml = '';
19687
-        var timeText;
19688
-        var titleHtml;
19689
-        classes.unshift('fc-day-grid-event', 'fc-h-event');
19690
-        // Only display a timed events time if it is the starting segment
19691
-        if (seg.isStart) {
19692
-            timeText = this.getTimeText(seg.footprint);
19693
-            if (timeText) {
19694
-                timeHtml = '<span class="fc-time">' + util_1.htmlEscape(timeText) + '</span>';
19695
-            }
19696
+    Toolbar.prototype.renderSection = function (position) {
19697
+        var _this = this;
19698
+        var calendar = this.calendar;
19699
+        var theme = calendar.theme;
19700
+        var optionsManager = calendar.optionsManager;
19701
+        var viewSpecManager = calendar.viewSpecManager;
19702
+        var sectionEl = $('<div class="fc-' + position + '">');
19703
+        var buttonStr = this.toolbarOptions.layout[position];
19704
+        var calendarCustomButtons = optionsManager.get('customButtons') || {};
19705
+        var calendarButtonTextOverrides = optionsManager.overrides.buttonText || {};
19706
+        var calendarButtonText = optionsManager.get('buttonText') || {};
19707
+        if (buttonStr) {
19708
+            $.each(buttonStr.split(' '), function (i, buttonGroupStr) {
19709
+                var groupChildren = $();
19710
+                var isOnlyButtons = true;
19711
+                var groupEl;
19712
+                $.each(buttonGroupStr.split(','), function (j, buttonName) {
19713
+                    var customButtonProps;
19714
+                    var viewSpec;
19715
+                    var buttonClick;
19716
+                    var buttonIcon; // only one of these will be set
19717
+                    var buttonText; // "
19718
+                    var buttonInnerHtml;
19719
+                    var buttonClasses;
19720
+                    var buttonEl;
19721
+                    var buttonAriaAttr;
19722
+                    if (buttonName === 'title') {
19723
+                        groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
19724
+                        isOnlyButtons = false;
19725
+                    }
19726
+                    else {
19727
+                        if ((customButtonProps = calendarCustomButtons[buttonName])) {
19728
+                            buttonClick = function (ev) {
19729
+                                if (customButtonProps.click) {
19730
+                                    customButtonProps.click.call(buttonEl[0], ev);
19731
+                                }
19732
+                            };
19733
+                            (buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) ||
19734
+                                (buttonIcon = theme.getIconClass(buttonName)) ||
19735
+                                (buttonText = customButtonProps.text);
19736
+                        }
19737
+                        else if ((viewSpec = viewSpecManager.getViewSpec(buttonName))) {
19738
+                            _this.viewsWithButtons.push(buttonName);
19739
+                            buttonClick = function () {
19740
+                                calendar.changeView(buttonName);
19741
+                            };
19742
+                            (buttonText = viewSpec.buttonTextOverride) ||
19743
+                                (buttonIcon = theme.getIconClass(buttonName)) ||
19744
+                                (buttonText = viewSpec.buttonTextDefault);
19745
+                        }
19746
+                        else if (calendar[buttonName]) { // a calendar method
19747
+                            buttonClick = function () {
19748
+                                calendar[buttonName]();
19749
+                            };
19750
+                            (buttonText = calendarButtonTextOverrides[buttonName]) ||
19751
+                                (buttonIcon = theme.getIconClass(buttonName)) ||
19752
+                                (buttonText = calendarButtonText[buttonName]);
19753
+                            //            ^ everything else is considered default
19754
+                        }
19755
+                        if (buttonClick) {
19756
+                            buttonClasses = [
19757
+                                'fc-' + buttonName + '-button',
19758
+                                theme.getClass('button'),
19759
+                                theme.getClass('stateDefault')
19760
+                            ];
19761
+                            if (buttonText) {
19762
+                                buttonInnerHtml = util_1.htmlEscape(buttonText);
19763
+                                buttonAriaAttr = '';
19764
+                            }
19765
+                            else if (buttonIcon) {
19766
+                                buttonInnerHtml = "<span class='" + buttonIcon + "'></span>";
19767
+                                buttonAriaAttr = ' aria-label="' + buttonName + '"';
19768
+                            }
19769
+                            buttonEl = $(// type="button" so that it doesn't submit a form
19770
+                            '<button type="button" class="' + buttonClasses.join(' ') + '"' +
19771
+                                buttonAriaAttr +
19772
+                                '>' + buttonInnerHtml + '</button>')
19773
+                                .click(function (ev) {
19774
+                                // don't process clicks for disabled buttons
19775
+                                if (!buttonEl.hasClass(theme.getClass('stateDisabled'))) {
19776
+                                    buttonClick(ev);
19777
+                                    // after the click action, if the button becomes the "active" tab, or disabled,
19778
+                                    // it should never have a hover class, so remove it now.
19779
+                                    if (buttonEl.hasClass(theme.getClass('stateActive')) ||
19780
+                                        buttonEl.hasClass(theme.getClass('stateDisabled'))) {
19781
+                                        buttonEl.removeClass(theme.getClass('stateHover'));
19782
+                                    }
19783
+                                }
19784
+                            })
19785
+                                .mousedown(function () {
19786
+                                // the *down* effect (mouse pressed in).
19787
+                                // only on buttons that are not the "active" tab, or disabled
19788
+                                buttonEl
19789
+                                    .not('.' + theme.getClass('stateActive'))
19790
+                                    .not('.' + theme.getClass('stateDisabled'))
19791
+                                    .addClass(theme.getClass('stateDown'));
19792
+                            })
19793
+                                .mouseup(function () {
19794
+                                // undo the *down* effect
19795
+                                buttonEl.removeClass(theme.getClass('stateDown'));
19796
+                            })
19797
+                                .hover(function () {
19798
+                                // the *hover* effect.
19799
+                                // only on buttons that are not the "active" tab, or disabled
19800
+                                buttonEl
19801
+                                    .not('.' + theme.getClass('stateActive'))
19802
+                                    .not('.' + theme.getClass('stateDisabled'))
19803
+                                    .addClass(theme.getClass('stateHover'));
19804
+                            }, function () {
19805
+                                // undo the *hover* effect
19806
+                                buttonEl
19807
+                                    .removeClass(theme.getClass('stateHover'))
19808
+                                    .removeClass(theme.getClass('stateDown')); // if mouseleave happens before mouseup
19809
+                            });
19810
+                            groupChildren = groupChildren.add(buttonEl);
19811
+                        }
19812
+                    }
19813
+                });
19814
+                if (isOnlyButtons) {
19815
+                    groupChildren
19816
+                        .first().addClass(theme.getClass('cornerLeft')).end()
19817
+                        .last().addClass(theme.getClass('cornerRight')).end();
19818
+                }
19819
+                if (groupChildren.length > 1) {
19820
+                    groupEl = $('<div>');
19821
+                    if (isOnlyButtons) {
19822
+                        groupEl.addClass(theme.getClass('buttonGroup'));
19823
+                    }
19824
+                    groupEl.append(groupChildren);
19825
+                    sectionEl.append(groupEl);
19826
+                }
19827
+                else {
19828
+                    sectionEl.append(groupChildren); // 1 or 0 children
19829
+                }
19830
+            });
19831
         }
19832
-        titleHtml =
19833
-            '<span class="fc-title">' +
19834
-                (util_1.htmlEscape(eventDef.title || '') || '&nbsp;') + // we always want one line of height
19835
-                '</span>';
19836
-        return '<a class="' + classes.join(' ') + '"' +
19837
-            (eventDef.url ?
19838
-                ' href="' + util_1.htmlEscape(eventDef.url) + '"' :
19839
-                '') +
19840
-            (skinCss ?
19841
-                ' style="' + skinCss + '"' :
19842
-                '') +
19843
-            '>' +
19844
-            '<div class="fc-content">' +
19845
-            (this.dayGrid.isRTL ?
19846
-                titleHtml + ' ' + timeHtml : // put a natural space in between
19847
-                timeHtml + ' ' + titleHtml //
19848
-            ) +
19849
-            '</div>' +
19850
-            (isResizableFromStart ?
19851
-                '<div class="fc-resizer fc-start-resizer" />' :
19852
-                '') +
19853
-            (isResizableFromEnd ?
19854
-                '<div class="fc-resizer fc-end-resizer" />' :
19855
-                '') +
19856
-            '</a>';
19857
+        return sectionEl;
19858
     };
19859
-    return DayGridEventRenderer;
19860
-}(EventRenderer_1.default));
19861
-exports.default = DayGridEventRenderer;
19862
-// Computes whether two segments' columns collide. They are assumed to be in the same row.
19863
-function isDaySegCollision(seg, otherSegs) {
19864
-    var i;
19865
-    var otherSeg;
19866
-    for (i = 0; i < otherSegs.length; i++) {
19867
-        otherSeg = otherSegs[i];
19868
-        if (otherSeg.leftCol <= seg.rightCol &&
19869
-            otherSeg.rightCol >= seg.leftCol) {
19870
-            return true;
19871
+    Toolbar.prototype.updateTitle = function (text) {
19872
+        if (this.el) {
19873
+            this.el.find('h2').text(text);
19874
         }
19875
-    }
19876
-    return false;
19877
-}
19878
-// A cmp function for determining the leftmost event
19879
-function compareDaySegCols(a, b) {
19880
-    return a.leftCol - b.leftCol;
19881
-}
19882
-
19883
-
19884
-/***/ }),
19885
-/* 251 */
19886
-/***/ (function(module, exports, __webpack_require__) {
19887
-
19888
-Object.defineProperty(exports, "__esModule", { value: true });
19889
-var tslib_1 = __webpack_require__(2);
19890
-var $ = __webpack_require__(3);
19891
-var HelperRenderer_1 = __webpack_require__(58);
19892
-var DayGridHelperRenderer = /** @class */ (function (_super) {
19893
-    tslib_1.__extends(DayGridHelperRenderer, _super);
19894
-    function DayGridHelperRenderer() {
19895
-        return _super !== null && _super.apply(this, arguments) || this;
19896
-    }
19897
-    // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
19898
-    DayGridHelperRenderer.prototype.renderSegs = function (segs, sourceSeg) {
19899
-        var helperNodes = [];
19900
-        var rowStructs;
19901
-        // TODO: not good to call eventRenderer this way
19902
-        rowStructs = this.eventRenderer.renderSegRows(segs);
19903
-        // inject each new event skeleton into each associated row
19904
-        this.component.rowEls.each(function (row, rowNode) {
19905
-            var rowEl = $(rowNode); // the .fc-row
19906
-            var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
19907
-            var skeletonTopEl;
19908
-            var skeletonTop;
19909
-            // If there is an original segment, match the top position. Otherwise, put it at the row's top level
19910
-            if (sourceSeg && sourceSeg.row === row) {
19911
-                skeletonTop = sourceSeg.el.position().top;
19912
-            }
19913
-            else {
19914
-                skeletonTopEl = rowEl.find('.fc-content-skeleton tbody');
19915
-                if (!skeletonTopEl.length) {
19916
-                    skeletonTopEl = rowEl.find('.fc-content-skeleton table');
19917
-                }
19918
-                skeletonTop = skeletonTopEl.position().top;
19919
-            }
19920
-            skeletonEl.css('top', skeletonTop)
19921
-                .find('table')
19922
-                .append(rowStructs[row].tbodyEl);
19923
-            rowEl.append(skeletonEl);
19924
-            helperNodes.push(skeletonEl[0]);
19925
-        });
19926
-        return $(helperNodes); // must return the elements rendered
19927
     };
19928
-    return DayGridHelperRenderer;
19929
-}(HelperRenderer_1.default));
19930
-exports.default = DayGridHelperRenderer;
19931
-
19932
-
19933
-/***/ }),
19934
-/* 252 */
19935
-/***/ (function(module, exports, __webpack_require__) {
19936
-
19937
-Object.defineProperty(exports, "__esModule", { value: true });
19938
-var tslib_1 = __webpack_require__(2);
19939
-var $ = __webpack_require__(3);
19940
-var FillRenderer_1 = __webpack_require__(57);
19941
-var DayGridFillRenderer = /** @class */ (function (_super) {
19942
-    tslib_1.__extends(DayGridFillRenderer, _super);
19943
-    function DayGridFillRenderer() {
19944
-        var _this = _super !== null && _super.apply(this, arguments) || this;
19945
-        _this.fillSegTag = 'td'; // override the default tag name
19946
-        return _this;
19947
-    }
19948
-    DayGridFillRenderer.prototype.attachSegEls = function (type, segs) {
19949
-        var nodes = [];
19950
-        var i;
19951
-        var seg;
19952
-        var skeletonEl;
19953
-        for (i = 0; i < segs.length; i++) {
19954
-            seg = segs[i];
19955
-            skeletonEl = this.renderFillRow(type, seg);
19956
-            this.component.rowEls.eq(seg.row).append(skeletonEl);
19957
-            nodes.push(skeletonEl[0]);
19958
+    Toolbar.prototype.activateButton = function (buttonName) {
19959
+        if (this.el) {
19960
+            this.el.find('.fc-' + buttonName + '-button')
19961
+                .addClass(this.calendar.theme.getClass('stateActive'));
19962
         }
19963
-        return nodes;
19964
     };
19965
-    // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
19966
-    DayGridFillRenderer.prototype.renderFillRow = function (type, seg) {
19967
-        var colCnt = this.component.colCnt;
19968
-        var startCol = seg.leftCol;
19969
-        var endCol = seg.rightCol + 1;
19970
-        var className;
19971
-        var skeletonEl;
19972
-        var trEl;
19973
-        if (type === 'businessHours') {
19974
-            className = 'bgevent';
19975
-        }
19976
-        else {
19977
-            className = type.toLowerCase();
19978
+    Toolbar.prototype.deactivateButton = function (buttonName) {
19979
+        if (this.el) {
19980
+            this.el.find('.fc-' + buttonName + '-button')
19981
+                .removeClass(this.calendar.theme.getClass('stateActive'));
19982
         }
19983
-        skeletonEl = $('<div class="fc-' + className + '-skeleton">' +
19984
-            '<table><tr/></table>' +
19985
-            '</div>');
19986
-        trEl = skeletonEl.find('tr');
19987
-        if (startCol > 0) {
19988
-            trEl.append('<td colspan="' + startCol + '"/>');
19989
+    };
19990
+    Toolbar.prototype.disableButton = function (buttonName) {
19991
+        if (this.el) {
19992
+            this.el.find('.fc-' + buttonName + '-button')
19993
+                .prop('disabled', true)
19994
+                .addClass(this.calendar.theme.getClass('stateDisabled'));
19995
         }
19996
-        trEl.append(seg.el.attr('colspan', endCol - startCol));
19997
-        if (endCol < colCnt) {
19998
-            trEl.append('<td colspan="' + (colCnt - endCol) + '"/>');
19999
+    };
20000
+    Toolbar.prototype.enableButton = function (buttonName) {
20001
+        if (this.el) {
20002
+            this.el.find('.fc-' + buttonName + '-button')
20003
+                .prop('disabled', false)
20004
+                .removeClass(this.calendar.theme.getClass('stateDisabled'));
20005
         }
20006
-        this.component.bookendCells(trEl);
20007
-        return skeletonEl;
20008
     };
20009
-    return DayGridFillRenderer;
20010
-}(FillRenderer_1.default));
20011
-exports.default = DayGridFillRenderer;
20012
+    Toolbar.prototype.getViewsWithButtons = function () {
20013
+        return this.viewsWithButtons;
20014
+    };
20015
+    return Toolbar;
20016
+}());
20017
+exports.default = Toolbar;
20018
 
20019
 
20020
 /***/ }),
20021
-/* 253 */
20022
+/* 258 */
20023
 /***/ (function(module, exports, __webpack_require__) {
20024
 
20025
 Object.defineProperty(exports, "__esModule", { value: true });
20026
 var tslib_1 = __webpack_require__(2);
20027
-var BasicViewDateProfileGenerator_1 = __webpack_require__(228);
20028
-var UnzonedRange_1 = __webpack_require__(5);
20029
-var MonthViewDateProfileGenerator = /** @class */ (function (_super) {
20030
-    tslib_1.__extends(MonthViewDateProfileGenerator, _super);
20031
-    function MonthViewDateProfileGenerator() {
20032
-        return _super !== null && _super.apply(this, arguments) || this;
20033
+var $ = __webpack_require__(3);
20034
+var util_1 = __webpack_require__(4);
20035
+var options_1 = __webpack_require__(33);
20036
+var locale_1 = __webpack_require__(32);
20037
+var Model_1 = __webpack_require__(51);
20038
+var OptionsManager = /** @class */ (function (_super) {
20039
+    tslib_1.__extends(OptionsManager, _super);
20040
+    function OptionsManager(_calendar, overrides) {
20041
+        var _this = _super.call(this) || this;
20042
+        _this._calendar = _calendar;
20043
+        _this.overrides = $.extend({}, overrides); // make a copy
20044
+        _this.dynamicOverrides = {};
20045
+        _this.compute();
20046
+        return _this;
20047
     }
20048
-    // Computes the date range that will be rendered.
20049
-    MonthViewDateProfileGenerator.prototype.buildRenderRange = function (currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
20050
-        var renderUnzonedRange = _super.prototype.buildRenderRange.call(this, currentUnzonedRange, currentRangeUnit, isRangeAllDay);
20051
-        var start = this.msToUtcMoment(renderUnzonedRange.startMs, isRangeAllDay);
20052
-        var end = this.msToUtcMoment(renderUnzonedRange.endMs, isRangeAllDay);
20053
-        var rowCnt;
20054
-        // ensure 6 weeks
20055
-        if (this.opt('fixedWeekCount')) {
20056
-            rowCnt = Math.ceil(// could be partial weeks due to hiddenDays
20057
-            end.diff(start, 'weeks', true) // dontRound=true
20058
-            );
20059
-            end.add(6 - rowCnt, 'weeks');
20060
+    OptionsManager.prototype.add = function (newOptionHash) {
20061
+        var optionCnt = 0;
20062
+        var optionName;
20063
+        this.recordOverrides(newOptionHash); // will trigger this model's watchers
20064
+        for (optionName in newOptionHash) {
20065
+            optionCnt++;
20066
+        }
20067
+        // special-case handling of single option change.
20068
+        // if only one option change, `optionName` will be its name.
20069
+        if (optionCnt === 1) {
20070
+            if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') {
20071
+                this._calendar.updateViewSize(true); // isResize=true
20072
+                return;
20073
+            }
20074
+            else if (optionName === 'defaultDate') {
20075
+                return; // can't change date this way. use gotoDate instead
20076
+            }
20077
+            else if (optionName === 'businessHours') {
20078
+                return; // this model already reacts to this
20079
+            }
20080
+            else if (/^(event|select)(Overlap|Constraint|Allow)$/.test(optionName)) {
20081
+                return; // doesn't affect rendering. only interactions.
20082
+            }
20083
+            else if (optionName === 'timezone') {
20084
+                this._calendar.view.flash('initialEvents');
20085
+                return;
20086
+            }
20087
+        }
20088
+        // catch-all. rerender the header and footer and rebuild/rerender the current view
20089
+        this._calendar.renderHeader();
20090
+        this._calendar.renderFooter();
20091
+        // even non-current views will be affected by this option change. do before rerender
20092
+        // TODO: detangle
20093
+        this._calendar.viewsByType = {};
20094
+        this._calendar.reinitView();
20095
+    };
20096
+    // Computes the flattened options hash for the calendar and assigns to `this.options`.
20097
+    // Assumes this.overrides and this.dynamicOverrides have already been initialized.
20098
+    OptionsManager.prototype.compute = function () {
20099
+        var locale;
20100
+        var localeDefaults;
20101
+        var isRTL;
20102
+        var dirDefaults;
20103
+        var rawOptions;
20104
+        locale = util_1.firstDefined(// explicit locale option given?
20105
+        this.dynamicOverrides.locale, this.overrides.locale);
20106
+        localeDefaults = locale_1.localeOptionHash[locale];
20107
+        if (!localeDefaults) { // explicit locale option not given or invalid?
20108
+            locale = options_1.globalDefaults.locale;
20109
+            localeDefaults = locale_1.localeOptionHash[locale] || {};
20110
+        }
20111
+        isRTL = util_1.firstDefined(// based on options computed so far, is direction RTL?
20112
+        this.dynamicOverrides.isRTL, this.overrides.isRTL, localeDefaults.isRTL, options_1.globalDefaults.isRTL);
20113
+        dirDefaults = isRTL ? options_1.rtlDefaults : {};
20114
+        this.dirDefaults = dirDefaults;
20115
+        this.localeDefaults = localeDefaults;
20116
+        rawOptions = options_1.mergeOptions([
20117
+            options_1.globalDefaults,
20118
+            dirDefaults,
20119
+            localeDefaults,
20120
+            this.overrides,
20121
+            this.dynamicOverrides
20122
+        ]);
20123
+        locale_1.populateInstanceComputableOptions(rawOptions); // fill in gaps with computed options
20124
+        this.reset(rawOptions);
20125
+    };
20126
+    // stores the new options internally, but does not rerender anything.
20127
+    OptionsManager.prototype.recordOverrides = function (newOptionHash) {
20128
+        var optionName;
20129
+        for (optionName in newOptionHash) {
20130
+            this.dynamicOverrides[optionName] = newOptionHash[optionName];
20131
         }
20132
-        return new UnzonedRange_1.default(start, end);
20133
+        this._calendar.viewSpecManager.clearCache(); // the dynamic override invalidates the options in this cache, so just clear it
20134
+        this.compute(); // this.options needs to be recomputed after the dynamic override
20135
     };
20136
-    return MonthViewDateProfileGenerator;
20137
-}(BasicViewDateProfileGenerator_1.default));
20138
-exports.default = MonthViewDateProfileGenerator;
20139
+    return OptionsManager;
20140
+}(Model_1.default));
20141
+exports.default = OptionsManager;
20142
 
20143
 
20144
 /***/ }),
20145
-/* 254 */
20146
+/* 259 */
20147
 /***/ (function(module, exports, __webpack_require__) {
20148
 
20149
 Object.defineProperty(exports, "__esModule", { value: true });
20150
-var tslib_1 = __webpack_require__(2);
20151
+var moment = __webpack_require__(0);
20152
+var $ = __webpack_require__(3);
20153
+var ViewRegistry_1 = __webpack_require__(24);
20154
 var util_1 = __webpack_require__(4);
20155
-var EventRenderer_1 = __webpack_require__(42);
20156
-var ListEventRenderer = /** @class */ (function (_super) {
20157
-    tslib_1.__extends(ListEventRenderer, _super);
20158
-    function ListEventRenderer() {
20159
-        return _super !== null && _super.apply(this, arguments) || this;
20160
+var options_1 = __webpack_require__(33);
20161
+var locale_1 = __webpack_require__(32);
20162
+var ViewSpecManager = /** @class */ (function () {
20163
+    function ViewSpecManager(optionsManager, _calendar) {
20164
+        this.optionsManager = optionsManager;
20165
+        this._calendar = _calendar;
20166
+        this.clearCache();
20167
     }
20168
-    ListEventRenderer.prototype.renderFgSegs = function (segs) {
20169
-        if (!segs.length) {
20170
-            this.component.renderEmptyMessage();
20171
-        }
20172
-        else {
20173
-            this.component.renderSegList(segs);
20174
-        }
20175
+    ViewSpecManager.prototype.clearCache = function () {
20176
+        this.viewSpecCache = {};
20177
     };
20178
-    // generates the HTML for a single event row
20179
-    ListEventRenderer.prototype.fgSegHtml = function (seg) {
20180
-        var view = this.view;
20181
-        var calendar = view.calendar;
20182
-        var theme = calendar.theme;
20183
-        var eventFootprint = seg.footprint;
20184
-        var eventDef = eventFootprint.eventDef;
20185
-        var componentFootprint = eventFootprint.componentFootprint;
20186
-        var url = eventDef.url;
20187
-        var classes = ['fc-list-item'].concat(this.getClasses(eventDef));
20188
-        var bgColor = this.getBgColor(eventDef);
20189
-        var timeHtml;
20190
-        if (componentFootprint.isAllDay) {
20191
-            timeHtml = view.getAllDayHtml();
20192
+    // Gets information about how to create a view. Will use a cache.
20193
+    ViewSpecManager.prototype.getViewSpec = function (viewType) {
20194
+        var cache = this.viewSpecCache;
20195
+        return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
20196
+    };
20197
+    // Given a duration singular unit, like "week" or "day", finds a matching view spec.
20198
+    // Preference is given to views that have corresponding buttons.
20199
+    ViewSpecManager.prototype.getUnitViewSpec = function (unit) {
20200
+        var viewTypes;
20201
+        var i;
20202
+        var spec;
20203
+        if ($.inArray(unit, util_1.unitsDesc) !== -1) {
20204
+            // put views that have buttons first. there will be duplicates, but oh well
20205
+            viewTypes = this._calendar.header.getViewsWithButtons(); // TODO: include footer as well?
20206
+            $.each(ViewRegistry_1.viewHash, function (viewType) {
20207
+                viewTypes.push(viewType);
20208
+            });
20209
+            for (i = 0; i < viewTypes.length; i++) {
20210
+                spec = this.getViewSpec(viewTypes[i]);
20211
+                if (spec) {
20212
+                    if (spec.singleUnit === unit) {
20213
+                        return spec;
20214
+                    }
20215
+                }
20216
+            }
20217
         }
20218
-        else if (view.isMultiDayRange(componentFootprint.unzonedRange)) {
20219
-            if (seg.isStart || seg.isEnd) {
20220
-                timeHtml = util_1.htmlEscape(this._getTimeText(calendar.msToMoment(seg.startMs), calendar.msToMoment(seg.endMs), componentFootprint.isAllDay));
20221
+    };
20222
+    // Builds an object with information on how to create a given view
20223
+    ViewSpecManager.prototype.buildViewSpec = function (requestedViewType) {
20224
+        var viewOverrides = this.optionsManager.overrides.views || {};
20225
+        var specChain = []; // for the view. lowest to highest priority
20226
+        var defaultsChain = []; // for the view. lowest to highest priority
20227
+        var overridesChain = []; // for the view. lowest to highest priority
20228
+        var viewType = requestedViewType;
20229
+        var spec; // for the view
20230
+        var overrides; // for the view
20231
+        var durationInput;
20232
+        var duration;
20233
+        var unit;
20234
+        // iterate from the specific view definition to a more general one until we hit an actual View class
20235
+        while (viewType) {
20236
+            spec = ViewRegistry_1.viewHash[viewType];
20237
+            overrides = viewOverrides[viewType];
20238
+            viewType = null; // clear. might repopulate for another iteration
20239
+            if (typeof spec === 'function') { // TODO: deprecate
20240
+                spec = { 'class': spec };
20241
             }
20242
-            else {
20243
-                timeHtml = view.getAllDayHtml();
20244
+            if (spec) {
20245
+                specChain.unshift(spec);
20246
+                defaultsChain.unshift(spec.defaults || {});
20247
+                durationInput = durationInput || spec.duration;
20248
+                viewType = viewType || spec.type;
20249
+            }
20250
+            if (overrides) {
20251
+                overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
20252
+                durationInput = durationInput || overrides.duration;
20253
+                viewType = viewType || overrides.type;
20254
             }
20255
         }
20256
-        else {
20257
-            // Display the normal time text for the *event's* times
20258
-            timeHtml = util_1.htmlEscape(this.getTimeText(eventFootprint));
20259
+        spec = util_1.mergeProps(specChain);
20260
+        spec.type = requestedViewType;
20261
+        if (!spec['class']) {
20262
+            return false;
20263
         }
20264
-        if (url) {
20265
-            classes.push('fc-has-url');
20266
+        // fall back to top-level `duration` option
20267
+        durationInput = durationInput ||
20268
+            this.optionsManager.dynamicOverrides.duration ||
20269
+            this.optionsManager.overrides.duration;
20270
+        if (durationInput) {
20271
+            duration = moment.duration(durationInput);
20272
+            if (duration.valueOf()) { // valid?
20273
+                unit = util_1.computeDurationGreatestUnit(duration, durationInput);
20274
+                spec.duration = duration;
20275
+                spec.durationUnit = unit;
20276
+                // view is a single-unit duration, like "week" or "day"
20277
+                // incorporate options for this. lowest priority
20278
+                if (duration.as(unit) === 1) {
20279
+                    spec.singleUnit = unit;
20280
+                    overridesChain.unshift(viewOverrides[unit] || {});
20281
+                }
20282
+            }
20283
         }
20284
-        return '<tr class="' + classes.join(' ') + '">' +
20285
-            (this.displayEventTime ?
20286
-                '<td class="fc-list-item-time ' + theme.getClass('widgetContent') + '">' +
20287
-                    (timeHtml || '') +
20288
-                    '</td>' :
20289
-                '') +
20290
-            '<td class="fc-list-item-marker ' + theme.getClass('widgetContent') + '">' +
20291
-            '<span class="fc-event-dot"' +
20292
-            (bgColor ?
20293
-                ' style="background-color:' + bgColor + '"' :
20294
-                '') +
20295
-            '></span>' +
20296
-            '</td>' +
20297
-            '<td class="fc-list-item-title ' + theme.getClass('widgetContent') + '">' +
20298
-            '<a' + (url ? ' href="' + util_1.htmlEscape(url) + '"' : '') + '>' +
20299
-            util_1.htmlEscape(eventDef.title || '') +
20300
-            '</a>' +
20301
-            '</td>' +
20302
-            '</tr>';
20303
+        spec.defaults = options_1.mergeOptions(defaultsChain);
20304
+        spec.overrides = options_1.mergeOptions(overridesChain);
20305
+        this.buildViewSpecOptions(spec);
20306
+        this.buildViewSpecButtonText(spec, requestedViewType);
20307
+        return spec;
20308
     };
20309
-    // like "4:00am"
20310
-    ListEventRenderer.prototype.computeEventTimeFormat = function () {
20311
-        return this.opt('mediumTimeFormat');
20312
+    // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
20313
+    ViewSpecManager.prototype.buildViewSpecOptions = function (spec) {
20314
+        var optionsManager = this.optionsManager;
20315
+        spec.options = options_1.mergeOptions([
20316
+            options_1.globalDefaults,
20317
+            spec.defaults,
20318
+            optionsManager.dirDefaults,
20319
+            optionsManager.localeDefaults,
20320
+            optionsManager.overrides,
20321
+            spec.overrides,
20322
+            optionsManager.dynamicOverrides // dynamically set via setter. highest precedence
20323
+        ]);
20324
+        locale_1.populateInstanceComputableOptions(spec.options);
20325
     };
20326
-    return ListEventRenderer;
20327
-}(EventRenderer_1.default));
20328
-exports.default = ListEventRenderer;
20329
-
20330
-
20331
-/***/ }),
20332
-/* 255 */
20333
-/***/ (function(module, exports, __webpack_require__) {
20334
-
20335
-Object.defineProperty(exports, "__esModule", { value: true });
20336
-var tslib_1 = __webpack_require__(2);
20337
-var $ = __webpack_require__(3);
20338
-var EventPointing_1 = __webpack_require__(59);
20339
-var ListEventPointing = /** @class */ (function (_super) {
20340
-    tslib_1.__extends(ListEventPointing, _super);
20341
-    function ListEventPointing() {
20342
-        return _super !== null && _super.apply(this, arguments) || this;
20343
-    }
20344
-    // for events with a url, the whole <tr> should be clickable,
20345
-    // but it's impossible to wrap with an <a> tag. simulate this.
20346
-    ListEventPointing.prototype.handleClick = function (seg, ev) {
20347
-        var url;
20348
-        _super.prototype.handleClick.call(this, seg, ev); // might prevent the default action
20349
-        // not clicking on or within an <a> with an href
20350
-        if (!$(ev.target).closest('a[href]').length) {
20351
-            url = seg.footprint.eventDef.url;
20352
-            if (url && !ev.isDefaultPrevented()) {
20353
-                window.location.href = url; // simulate link click
20354
-            }
20355
+    // Computes and assigns a view spec's buttonText-related options
20356
+    ViewSpecManager.prototype.buildViewSpecButtonText = function (spec, requestedViewType) {
20357
+        var optionsManager = this.optionsManager;
20358
+        // given an options object with a possible `buttonText` hash, lookup the buttonText for the
20359
+        // requested view, falling back to a generic unit entry like "week" or "day"
20360
+        function queryButtonText(options) {
20361
+            var buttonText = options.buttonText || {};
20362
+            return buttonText[requestedViewType] ||
20363
+                // view can decide to look up a certain key
20364
+                (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) ||
20365
+                // a key like "month"
20366
+                (spec.singleUnit ? buttonText[spec.singleUnit] : null);
20367
         }
20368
+        // highest to lowest priority
20369
+        spec.buttonTextOverride =
20370
+            queryButtonText(optionsManager.dynamicOverrides) ||
20371
+                queryButtonText(optionsManager.overrides) || // constructor-specified buttonText lookup hash takes precedence
20372
+                spec.overrides.buttonText; // `buttonText` for view-specific options is a string
20373
+        // highest to lowest priority. mirrors buildViewSpecOptions
20374
+        spec.buttonTextDefault =
20375
+            queryButtonText(optionsManager.localeDefaults) ||
20376
+                queryButtonText(optionsManager.dirDefaults) ||
20377
+                spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
20378
+                queryButtonText(options_1.globalDefaults) ||
20379
+                (spec.duration ? this._calendar.humanizeDuration(spec.duration) : null) || // like "3 days"
20380
+                requestedViewType; // fall back to given view name
20381
     };
20382
-    return ListEventPointing;
20383
-}(EventPointing_1.default));
20384
-exports.default = ListEventPointing;
20385
+    return ViewSpecManager;
20386
+}());
20387
+exports.default = ViewSpecManager;
20388
 
20389
 
20390
 /***/ }),
20391
-/* 256 */
20392
+/* 260 */
20393
 /***/ (function(module, exports, __webpack_require__) {
20394
 
20395
 Object.defineProperty(exports, "__esModule", { value: true });
20396
 var EventSourceParser_1 = __webpack_require__(38);
20397
-var ArrayEventSource_1 = __webpack_require__(52);
20398
-var FuncEventSource_1 = __webpack_require__(215);
20399
-var JsonFeedEventSource_1 = __webpack_require__(216);
20400
+var ArrayEventSource_1 = __webpack_require__(56);
20401
+var FuncEventSource_1 = __webpack_require__(223);
20402
+var JsonFeedEventSource_1 = __webpack_require__(224);
20403
 EventSourceParser_1.default.registerClass(ArrayEventSource_1.default);
20404
 EventSourceParser_1.default.registerClass(FuncEventSource_1.default);
20405
 EventSourceParser_1.default.registerClass(JsonFeedEventSource_1.default);
20406
 
20407
 
20408
 /***/ }),
20409
-/* 257 */
20410
+/* 261 */
20411
 /***/ (function(module, exports, __webpack_require__) {
20412
 
20413
 Object.defineProperty(exports, "__esModule", { value: true });
20414
-var ThemeRegistry_1 = __webpack_require__(51);
20415
-var StandardTheme_1 = __webpack_require__(213);
20416
-var JqueryUiTheme_1 = __webpack_require__(214);
20417
-var Bootstrap3Theme_1 = __webpack_require__(258);
20418
-var Bootstrap4Theme_1 = __webpack_require__(259);
20419
+var ThemeRegistry_1 = __webpack_require__(57);
20420
+var StandardTheme_1 = __webpack_require__(221);
20421
+var JqueryUiTheme_1 = __webpack_require__(222);
20422
+var Bootstrap3Theme_1 = __webpack_require__(262);
20423
+var Bootstrap4Theme_1 = __webpack_require__(263);
20424
 ThemeRegistry_1.defineThemeSystem('standard', StandardTheme_1.default);
20425
 ThemeRegistry_1.defineThemeSystem('jquery-ui', JqueryUiTheme_1.default);
20426
 ThemeRegistry_1.defineThemeSystem('bootstrap3', Bootstrap3Theme_1.default);
20427
@@ -14801,12 +14939,12 @@
20428
 
20429
 
20430
 /***/ }),
20431
-/* 258 */
20432
+/* 262 */
20433
 /***/ (function(module, exports, __webpack_require__) {
20434
 
20435
 Object.defineProperty(exports, "__esModule", { value: true });
20436
 var tslib_1 = __webpack_require__(2);
20437
-var Theme_1 = __webpack_require__(19);
20438
+var Theme_1 = __webpack_require__(22);
20439
 var Bootstrap3Theme = /** @class */ (function (_super) {
20440
     tslib_1.__extends(Bootstrap3Theme, _super);
20441
     function Bootstrap3Theme() {
20442
@@ -14850,12 +14988,12 @@
20443
 
20444
 
20445
 /***/ }),
20446
-/* 259 */
20447
+/* 263 */
20448
 /***/ (function(module, exports, __webpack_require__) {
20449
 
20450
 Object.defineProperty(exports, "__esModule", { value: true });
20451
 var tslib_1 = __webpack_require__(2);
20452
-var Theme_1 = __webpack_require__(19);
20453
+var Theme_1 = __webpack_require__(22);
20454
 var Bootstrap4Theme = /** @class */ (function (_super) {
20455
     tslib_1.__extends(Bootstrap4Theme, _super);
20456
     function Bootstrap4Theme() {
20457
@@ -14899,13 +15037,13 @@
20458
 
20459
 
20460
 /***/ }),
20461
-/* 260 */
20462
+/* 264 */
20463
 /***/ (function(module, exports, __webpack_require__) {
20464
 
20465
 Object.defineProperty(exports, "__esModule", { value: true });
20466
-var ViewRegistry_1 = __webpack_require__(22);
20467
-var BasicView_1 = __webpack_require__(62);
20468
-var MonthView_1 = __webpack_require__(229);
20469
+var ViewRegistry_1 = __webpack_require__(24);
20470
+var BasicView_1 = __webpack_require__(67);
20471
+var MonthView_1 = __webpack_require__(246);
20472
 ViewRegistry_1.defineView('basic', {
20473
     'class': BasicView_1.default
20474
 });
20475
@@ -14927,12 +15065,12 @@
20476
 
20477
 
20478
 /***/ }),
20479
-/* 261 */
20480
+/* 265 */
20481
 /***/ (function(module, exports, __webpack_require__) {
20482
 
20483
 Object.defineProperty(exports, "__esModule", { value: true });
20484
-var ViewRegistry_1 = __webpack_require__(22);
20485
-var AgendaView_1 = __webpack_require__(226);
20486
+var ViewRegistry_1 = __webpack_require__(24);
20487
+var AgendaView_1 = __webpack_require__(238);
20488
 ViewRegistry_1.defineView('agenda', {
20489
     'class': AgendaView_1.default,
20490
     defaults: {
20491
@@ -14952,12 +15090,12 @@
20492
 
20493
 
20494
 /***/ }),
20495
-/* 262 */
20496
+/* 266 */
20497
 /***/ (function(module, exports, __webpack_require__) {
20498
 
20499
 Object.defineProperty(exports, "__esModule", { value: true });
20500
-var ViewRegistry_1 = __webpack_require__(22);
20501
-var ListView_1 = __webpack_require__(230);
20502
+var ViewRegistry_1 = __webpack_require__(24);
20503
+var ListView_1 = __webpack_require__(248);
20504
 ViewRegistry_1.defineView('list', {
20505
     'class': ListView_1.default,
20506
     buttonTextKey: 'list',
20507
@@ -14999,7 +15137,7 @@
20508
 
20509
 
20510
 /***/ }),
20511
-/* 263 */
20512
+/* 267 */
20513
 /***/ (function(module, exports) {
20514
 
20515
 Object.defineProperty(exports, "__esModule", { value: true });
20516
iRony-0.4.3.tar.gz/lib/plugins/kolab_addressbook/composer.json -> iRony-0.4.4.tar.gz/lib/plugins/kolab_addressbook/composer.json Changed
10
 
1
@@ -4,7 +4,7 @@
2
     "description": "Kolab addressbook",
3
     "homepage": "https://git.kolab.org/diffusion/RPK/",
4
     "license": "AGPLv3",
5
-    "version": "3.5.2",
6
+    "version": "3.5.5",
7
     "authors": [
8
         {
9
             "name": "Thomas Bruederli",
10
iRony-0.4.3.tar.gz/lib/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php -> iRony-0.4.4.tar.gz/lib/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php Changed
10
 
1
@@ -183,7 +183,7 @@
2
         if ($action == 'edit') {
3
             $path_imap = explode($delim, $folder);
4
             $name      = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP');
5
-            $path_imap = implode($path_imap, $delim);
6
+            $path_imap = implode($delim, $path_imap);
7
         }
8
         else { // create
9
             $path_imap = $folder;
10
iRony-0.4.3.tar.gz/lib/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php -> iRony-0.4.4.tar.gz/lib/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php Changed
10
 
1
@@ -460,7 +460,7 @@
2
 
3
         $scount = count($fields);
4
         // build key name regexp
5
-        $regexp = '/^(' . implode($fields, '|') . ')(?:.*)$/';
6
+        $regexp = '/^(' . implode('|', $fields) . ')(?:.*)$/';
7
 
8
         // pass query to storage if only indexed cols are involved
9
         // NOTE: this is only some rough pre-filtering but probably includes false positives
10
iRony-0.4.3.tar.gz/lib/plugins/kolab_auth/composer.json -> iRony-0.4.4.tar.gz/lib/plugins/kolab_auth/composer.json Changed
10
 
1
@@ -4,7 +4,7 @@
2
     "description": "Kolab authentication",
3
     "homepage": "https://git.kolab.org/diffusion/RPK/",
4
     "license": "AGPLv3",
5
-    "version": "3.5.2",
6
+    "version": "3.5.4",
7
     "authors": [
8
         {
9
             "name": "Thomas Bruederli",
10
iRony-0.4.3.tar.gz/lib/plugins/kolab_chat/composer.json -> iRony-0.4.4.tar.gz/lib/plugins/kolab_chat/composer.json Changed
10
 
1
@@ -4,7 +4,7 @@
2
     "description": "Chat integration plugin",
3
     "homepage": "https://git.kolab.org/diffusion/RPK/",
4
     "license": "AGPLv3",
5
-    "version": "3.5.2",
6
+    "version": "3.5.4",
7
     "authors": [
8
         {
9
             "name": "Aleksander Machniak",
10
iRony-0.4.3.tar.gz/lib/plugins/kolab_chat/drivers/mattermost.php -> iRony-0.4.4.tar.gz/lib/plugins/kolab_chat/drivers/mattermost.php Changed
10
 
1
@@ -347,7 +347,7 @@
2
         }
3
 
4
         $cookie = session_get_cookie_params();
5
-        $secure = $cookie['secure'] || self::https_check();
6
+        $secure = $cookie['secure'] || rcube_utils::https_check();
7
 
8
         if ($domain = $this->rc->config->get('kolab_chat_session_domain')) {
9
             $cookie['domain'] = $domain;
10
iRony-0.4.3.tar.gz/lib/plugins/kolab_notes/composer.json -> iRony-0.4.4.tar.gz/lib/plugins/kolab_notes/composer.json Changed
10
 
1
@@ -4,7 +4,7 @@
2
     "description": "Notes module for Roundcube connecting to a Kolab server for storage",
3
     "homepage": "https://git.kolab.org/diffusion/RPK/",
4
     "license": "AGPLv3",
5
-    "version": "3.5.2",
6
+    "version": "3.5.5",
7
     "authors": [
8
         {
9
             "name": "Thomas Bruederli",
10
iRony-0.4.3.tar.gz/lib/plugins/kolab_notes/kolab_notes_ui.php -> iRony-0.4.4.tar.gz/lib/plugins/kolab_notes/kolab_notes_ui.php Changed
10
 
1
@@ -306,7 +306,7 @@
2
 
3
             $path_imap = explode($delim, $folder_name);
4
             array_pop($path_imap);  // pop off name part
5
-            $path_imap = implode($path_imap, $delim);
6
+            $path_imap = implode($delim, $path_imap);
7
         }
8
         else {
9
             $path_imap = '';
10
iRony-0.4.3.tar.gz/lib/plugins/kolab_sso/composer.json -> iRony-0.4.4.tar.gz/lib/plugins/kolab_sso/composer.json Changed
10
 
1
@@ -4,7 +4,7 @@
2
     "description": "Single Sign On for Kolab",
3
     "homepage": "https://git.kolab.org/diffusion/RPK/",
4
     "license": "AGPLv3",
5
-    "version": "3.5.2",
6
+    "version": "3.5.4",
7
     "authors": [
8
         {
9
             "name": "Aleksander Machniak",
10
iRony-0.4.3.tar.gz/lib/plugins/libkolab/bin/modcache.sh -> iRony-0.4.4.tar.gz/lib/plugins/libkolab/bin/modcache.sh Changed
50
 
1
@@ -23,12 +23,9 @@
2
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
3
  */
4
 
5
-define('INSTALL_PATH', realpath('.') . '/' );
6
+define('INSTALL_PATH', __DIR__ . '/../../../');
7
 ini_set('display_errors', 1);
8
 
9
-if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
10
-    die("Execute this from the Roundcube installation dir!\n\n");
11
-
12
 require_once INSTALL_PATH . 'program/include/clisetup.php';
13
 
14
 function print_usage()
15
@@ -58,6 +55,10 @@
16
 
17
 $rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
18
 
19
+// Make --host argument optional where the default_host is a simple string
20
+if (empty($opts['host'])) {
21
+    $opts['host'] = imap_host();
22
+}
23
 
24
 // connect to database
25
 $db = $rcmail->get_dbh();
26
@@ -236,3 +237,23 @@
27
     return $auth['valid'];
28
 }
29
 
30
+function imap_host()
31
+{
32
+    global $rcmail;
33
+
34
+    $default_host = $rcmail->config->get('default_host');
35
+
36
+    if (is_array($default_host)) {
37
+        $key = key($default_host);
38
+        $imap_host = is_numeric($key) ? $default_host[$key] : $key;
39
+    }
40
+    else {
41
+        $imap_host = $default_host;
42
+    }
43
+
44
+    // strip protocol prefix
45
+    $uri = parse_url($imap_host);
46
+    if (!empty($uri['host'])) {
47
+        return $uri['host'];
48
+    }
49
+}
50
iRony-0.4.3.tar.gz/lib/plugins/libkolab/bin/randomcontacts.sh -> iRony-0.4.4.tar.gz/lib/plugins/libkolab/bin/randomcontacts.sh Changed
60
 
1
@@ -23,18 +23,15 @@
2
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
3
  */
4
 
5
-define('INSTALL_PATH', realpath('.') . '/' );
6
+define('INSTALL_PATH', __DIR__ . '/../../../');
7
 ini_set('display_errors', 1);
8
 
9
-if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
10
-    die("Execute this from the Roundcube installation dir!\n\n");
11
-
12
 require_once INSTALL_PATH . 'program/include/clisetup.php';
13
 
14
 function print_usage()
15
 {
16
     print "Usage:  randomcontacts.sh [OPTIONS] USERNAME FOLDER\n";
17
-    print "Create random contact that for then given user in the specified folder.\n";
18
+    print "Create random contact for a given user in a specified folder.\n";
19
     print "-n, --num      Number of contacts to be created, defaults to 50\n";
20
     print "-h, --host     IMAP host name\n";
21
     print "-p, --password IMAP user password\n";
22
@@ -56,11 +53,8 @@
23
 $rcmail->plugins->load_plugins(array('libkolab'));
24
 ini_set('display_errors', 1);
25
 
26
-
27
 if (empty($opts['host'])) {
28
-    $opts['host'] = $rcmail->config->get('default_host');
29
-    if (is_array($opts['host']))  // not unique
30
-        $opts['host'] = null;
31
+    $opts['host'] = imap_host();
32
 }
33
 
34
 if (empty($opts['username']) || empty($opts['folder']) || empty($opts['host'])) {
35
@@ -179,3 +173,24 @@
36
 
37
     return rtrim($str);
38
 }
39
+
40
+function imap_host()
41
+{
42
+    global $rcmail;
43
+
44
+    $default_host = $rcmail->config->get('default_host');
45
+
46
+    if (is_array($default_host)) {
47
+        $key = key($default_host);
48
+        $imap_host = is_numeric($key) ? $default_host[$key] : $key;
49
+    }
50
+    else {
51
+        $imap_host = $default_host;
52
+    }
53
+
54
+    // strip protocol prefix
55
+    $uri = parse_url($imap_host);
56
+    if (!empty($uri['host'])) {
57
+        return $uri['host'];
58
+    }
59
+}
60
iRony-0.4.3.tar.gz/lib/plugins/libkolab/bin/readcache.sh -> iRony-0.4.4.tar.gz/lib/plugins/libkolab/bin/readcache.sh Changed
105
 
1
@@ -22,13 +22,10 @@
2
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
3
  */
4
 
5
-define('INSTALL_PATH', realpath('.') . '/' );
6
+define('INSTALL_PATH', __DIR__ . '/../../../');
7
 ini_set('display_errors', 1);
8
 libxml_use_internal_errors(true);
9
 
10
-if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
11
-    die("Execute this from the Roundcube installation dir!\n\n");
12
-
13
 require_once INSTALL_PATH . 'program/include/clisetup.php';
14
 
15
 function print_usage()
16
@@ -51,17 +48,7 @@
17
 $rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
18
 
19
 if (empty($imap_host)) {
20
-    $default_host = $rcmail->config->get('default_host');
21
-    if (is_array($default_host)) {
22
-        list($k,$v) = each($default_host);
23
-        $imap_host = is_numeric($k) ? $v : $k;
24
-    }
25
-    else {
26
-        $imap_host = $default_host;
27
-    }
28
-
29
-    // strip protocol prefix
30
-    $imap_host = preg_replace('!^[a-z]+://!', '', $imap_host);
31
+    $imap_host = imap_host();
32
 }
33
 
34
 if (empty($folder) || empty($imap_host)) {
35
@@ -72,8 +59,9 @@
36
 // connect to database
37
 $db = $rcmail->get_dbh();
38
 $db->db_connect('r');
39
-if (!$db->is_connected() || $db->is_error())
40
+if (!$db->is_connected() || $db->is_error()) {
41
     die("No DB connection\n");
42
+}
43
 
44
 
45
 // resolve folder_id
46
@@ -118,11 +106,13 @@
47
 $extra_cols_ = $extra_cols[$folder_data['type']] ?: array();
48
 $sql_arr = $db->fetch_assoc($db->query("SELECT COUNT(*) as cnt FROM `$cache_table` WHERE `folder_id`=?", intval($folder_id)));
49
 
50
-print "CTag  = " . $folder_data['ctag'] . "\n";
51
-print "Lock  = " . $folder_data['synclock'] . "\n";
52
-print "Count = " . $sql_arr['cnt'] . "\n";
53
+print "CTag     = " . $folder_data['ctag'] . "\n";
54
+print "Lock     = " . $folder_data['synclock'] . "\n";
55
+print "Changed  = " . $folder_data['changed'] . "\n";
56
+print "ObjCount = " . $folder_data['objectcount'] . "\n";
57
+print "Count    = " . $sql_arr['cnt'] . "\n";
58
 print "----------------------------------------------------------------------------------\n";
59
-print "<MSG>\t<UUID>\t<CHANGED>\t<DATA>\t<XML>\t";
60
+print "<MSG>\t<UUID>\t<CHANGED>\t<DATA>\t";
61
 print join("\t", array_map(function($c) { return '<' . strtoupper($c) . '>'; }, $extra_cols_));
62
 print "\n----------------------------------------------------------------------------------\n";
63
 
64
@@ -131,12 +121,8 @@
65
     print $sql_arr['msguid'] . "\t" . $sql_arr['uid'] . "\t" . $sql_arr['changed'];
66
 
67
     // try to unserialize data block
68
-    $object = @unserialize(@base64_decode($sql_arr['data']));
69
-    print "\t" . ($object === false ? 'FAIL!' : ($object['uid'] == $sql_arr['uid'] ? 'OK' : '!!!'));
70
-
71
-    // check XML validity
72
-    $xml = simplexml_load_string($sql_arr['xml']);
73
-    print "\t" . ($xml === false ? 'FAIL!' : 'OK');
74
+    $object = json_decode($sql_arr['data']);
75
+    print "\t" . ($object === false ? 'FAIL!' : 'OK');
76
 
77
     // print extra cols
78
     array_walk($extra_cols_, function($c) use ($sql_arr) {
79
@@ -148,3 +134,25 @@
80
 
81
 print "----------------------------------------------------------------------------------\n";
82
 echo "Done.\n";
83
+
84
+
85
+function imap_host()
86
+{
87
+    global $rcmail;
88
+
89
+    $default_host = $rcmail->config->get('default_host');
90
+
91
+    if (is_array($default_host)) {
92
+        $key = key($default_host);
93
+        $imap_host = is_numeric($key) ? $default_host[$key] : $key;
94
+    }
95
+    else {
96
+        $imap_host = $default_host;
97
+    }
98
+
99
+    // strip protocol prefix
100
+    $uri = parse_url($imap_host);
101
+    if (!empty($uri['host'])) {
102
+        return $uri['host'];
103
+    }
104
+}
105
iRony-0.4.3.tar.gz/lib/plugins/libkolab/composer.json -> iRony-0.4.4.tar.gz/lib/plugins/libkolab/composer.json Changed
10
 
1
@@ -4,7 +4,7 @@
2
     "description": "Plugin to setup a basic environment for the interaction with a Kolab server.",
3
     "homepage": "https://git.kolab.org/diffusion/RPK/",
4
     "license": "AGPLv3",
5
-    "version": "3.5.2",
6
+    "version": "3.5.5",
7
     "authors": [
8
         {
9
             "name": "Thomas Bruederli",
10
iRony-0.4.3.tar.gz/lib/plugins/libkolab/lib/kolab_format_xcal.php -> iRony-0.4.4.tar.gz/lib/plugins/libkolab/lib/kolab_format_xcal.php Changed
21
 
1
@@ -203,7 +203,7 @@
2
                 for ($i=0; $i < $byday->size(); $i++) {
3
                     $daypos = $byday->get($i);
4
                     $prefix = $daypos->occurence();
5
-                    $weekdays[] = ($prefix ? $prefix : '') . $weekday_map[$daypos->weekday()];
6
+                    $weekdays[] = ($prefix ?: '') . $weekday_map[$daypos->weekday()];
7
                 }
8
                 $object['recurrence']['BYDAY'] = join(',', $weekdays);
9
             }
10
@@ -382,8 +382,8 @@
11
                 $att = new Attendee;
12
                 $att->setContact($cr);
13
                 $att->setPartStat($this->part_status_map[$attendee['status']]);
14
-                $att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
15
-                $att->setCutype($this->cutype_map[$attendee['cutype']] ? $this->cutype_map[$attendee['cutype']] : kolabformat::CutypeIndividual);
16
+                $att->setRole($this->role_map[$attendee['role']] ?: kolabformat::Required);
17
+                $att->setCutype($this->cutype_map[$attendee['cutype']] ?: kolabformat::CutypeIndividual);
18
                 $att->setRSVP((bool)$attendee['rsvp']);
19
 
20
                 if (!empty($attendee['delegated-from'])) {
21
iRony-0.4.3.tar.gz/lib/plugins/libkolab/lib/kolab_storage.php -> iRony-0.4.4.tar.gz/lib/plugins/libkolab/lib/kolab_storage.php Changed
25
 
1
@@ -1371,8 +1371,8 @@
2
                 self::$subscriptions = self::$imap->list_folders_subscribed();
3
                 self::$with_tempsubs = true;
4
             }
5
-            self::$states = self::$subscriptions;
6
-            $folders = implode(self::$states, '**');
7
+            self::$states = (array) self::$subscriptions;
8
+            $folders = implode('**', self::$states);
9
             $rcube->user->save_prefs(array('kolab_active_folders' => $folders));
10
         }
11
 
12
@@ -1396,9 +1396,9 @@
13
         }
14
 
15
         // update user preferences
16
-        $folders = implode(self::$states, '**');
17
-        $rcube   = rcube::get_instance();
18
-        return $rcube->user->save_prefs(array('kolab_active_folders' => $folders));
19
+        $folders = implode('**', self::$states);
20
+
21
+        return rcube::get_instance()->user->save_prefs(array('kolab_active_folders' => $folders));
22
     }
23
 
24
     /**
25
iRony-0.4.3.tar.gz/lib/plugins/tasklist/composer.json -> iRony-0.4.4.tar.gz/lib/plugins/tasklist/composer.json Changed
10
 
1
@@ -4,7 +4,7 @@
2
     "description": "Task management plugin",
3
     "homepage": "https://git.kolab.org/diffusion/RPK/",
4
     "license": "AGPLv3",
5
-    "version": "3.5.2",
6
+    "version": "3.5.4",
7
     "authors": [
8
         {
9
             "name": "Thomas Bruederli",
10
iRony-0.4.3.tar.gz/vendor/autoload.php -> iRony-0.4.4.tar.gz/vendor/autoload.php Changed
7
 
1
@@ -4,4 +4,4 @@
2
 
3
 require_once __DIR__ . '/composer/autoload_real.php';
4
 
5
-return ComposerAutoloaderInit19391cad60298ed18ee44f901f020bb5::getLoader();
6
+return ComposerAutoloaderInitcdf8a7f128c335357e9ae823f2040e43::getLoader();
7
iRony-0.4.3.tar.gz/vendor/composer/ClassLoader.php -> iRony-0.4.4.tar.gz/vendor/composer/ClassLoader.php Changed
10
 
1
@@ -60,7 +60,7 @@
2
     public function getPrefixes()
3
     {
4
         if (!empty($this->prefixesPsr0)) {
5
-            return call_user_func_array('array_merge', $this->prefixesPsr0);
6
+            return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
7
         }
8
 
9
         return array();
10
iRony-0.4.3.tar.gz/vendor/composer/autoload_real.php -> iRony-0.4.4.tar.gz/vendor/composer/autoload_real.php Changed
38
 
1
@@ -2,7 +2,7 @@
2
 
3
 // autoload_real.php @generated by Composer
4
 
5
-class ComposerAutoloaderInit19391cad60298ed18ee44f901f020bb5
6
+class ComposerAutoloaderInitcdf8a7f128c335357e9ae823f2040e43
7
 {
8
     private static $loader;
9
 
10
@@ -13,21 +13,24 @@
11
         }
12
     }
13
 
14
+    /**
15
+     * @return \Composer\Autoload\ClassLoader
16
+     */
17
     public static function getLoader()
18
     {
19
         if (null !== self::$loader) {
20
             return self::$loader;
21
         }
22
 
23
-        spl_autoload_register(array('ComposerAutoloaderInit19391cad60298ed18ee44f901f020bb5', 'loadClassLoader'), true, true);
24
+        spl_autoload_register(array('ComposerAutoloaderInitcdf8a7f128c335357e9ae823f2040e43', 'loadClassLoader'), true, true);
25
         self::$loader = $loader = new \Composer\Autoload\ClassLoader();
26
-        spl_autoload_unregister(array('ComposerAutoloaderInit19391cad60298ed18ee44f901f020bb5', 'loadClassLoader'));
27
+        spl_autoload_unregister(array('ComposerAutoloaderInitcdf8a7f128c335357e9ae823f2040e43', 'loadClassLoader'));
28
 
29
         $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
30
         if ($useStaticLoader) {
31
             require_once __DIR__ . '/autoload_static.php';
32
 
33
-            call_user_func(\Composer\Autoload\ComposerStaticInit19391cad60298ed18ee44f901f020bb5::getInitializer($loader));
34
+            call_user_func(\Composer\Autoload\ComposerStaticInitcdf8a7f128c335357e9ae823f2040e43::getInitializer($loader));
35
         } else {
36
             $map = require __DIR__ . '/autoload_namespaces.php';
37
             foreach ($map as $namespace => $path) {
38
iRony-0.4.3.tar.gz/vendor/composer/autoload_static.php -> iRony-0.4.4.tar.gz/vendor/composer/autoload_static.php Changed
21
 
1
@@ -4,7 +4,7 @@
2
 
3
 namespace Composer\Autoload;
4
 
5
-class ComposerStaticInit19391cad60298ed18ee44f901f020bb5
6
+class ComposerStaticInitcdf8a7f128c335357e9ae823f2040e43
7
 {
8
     public static $prefixLengthsPsr4 = array (
9
         'S' => 
10
@@ -53,8 +53,8 @@
11
     public static function getInitializer(ClassLoader $loader)
12
     {
13
         return \Closure::bind(function () use ($loader) {
14
-            $loader->prefixLengthsPsr4 = ComposerStaticInit19391cad60298ed18ee44f901f020bb5::$prefixLengthsPsr4;
15
-            $loader->prefixDirsPsr4 = ComposerStaticInit19391cad60298ed18ee44f901f020bb5::$prefixDirsPsr4;
16
+            $loader->prefixLengthsPsr4 = ComposerStaticInitcdf8a7f128c335357e9ae823f2040e43::$prefixLengthsPsr4;
17
+            $loader->prefixDirsPsr4 = ComposerStaticInitcdf8a7f128c335357e9ae823f2040e43::$prefixDirsPsr4;
18
 
19
         }, null, ClassLoader::class);
20
     }
21
iRony.dsc Changed
17
 
1
@@ -2,7 +2,7 @@
2
 Source: irony
3
 Binary: irony
4
 Architecture: all
5
-Version: 0.4.3-1~kolab2
6
+Version: 0.4.4-1~kolab1
7
 Maintainer: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com>
8
 Uploaders: Paul Klos <kolab@klos2day.nl>
9
 Homepage: http://www.kolab.org/
10
@@ -22,5 +22,5 @@
11
 Package-List:
12
  iRony deb admin extra
13
 Files:
14
- 00000000000000000000000000000000 0 iRony-0.4.3.tar.gz
15
+ 00000000000000000000000000000000 0 iRony-0.4.4.tar.gz
16
  00000000000000000000000000000000 0 debian.tar.gz
17
Refresh
Refresh
No rpmlint log
Request History
Jeroen van Meeuwen's avatar

vanmeeuwen created request over 4 years ago

Check in 0.4.4


Jeroen van Meeuwen's avatar

vanmeeuwen accepted review over 4 years ago

Accept


Jeroen van Meeuwen's avatar

vanmeeuwen accepted review over 4 years ago

Accept


Jeroen van Meeuwen's avatar

vanmeeuwen approved review over 4 years ago

Accept


Jeroen van Meeuwen's avatar

vanmeeuwen accepted request over 4 years ago

Accept