瀏覽代碼

Merge pull request #74 from buxx/master

Skylsmoi 8 年之前
父節點
當前提交
b5920ed809
共有 100 個文件被更改,包括 83343 次插入452 次删除
  1. 1 0
      .gitignore
  2. 4 0
      .travis.yml
  3. 451 400
      README.md
  4. 3 1
      install/requirements.txt
  5. 4 0
      tracim/development.ini.base
  6. 28 0
      tracim/migration/versions/b4b8d57b54e5_add_hash_column_for_digest_.py
  7. 28 0
      tracim/migration/versions/bdb195ed95bb_user_auth_token_columns.py
  8. 1 0
      tracim/setup.py
  9. 3 3
      tracim/test.ini
  10. 3 1
      tracim/tracim/command/__init__.py
  11. 26 1
      tracim/tracim/config/app_cfg.py
  12. 3 0
      tracim/tracim/controllers/admin/user.py
  13. 5 2
      tracim/tracim/controllers/admin/workspace.py
  14. 58 0
      tracim/tracim/controllers/calendar.py
  15. 5 1
      tracim/tracim/controllers/root.py
  16. 1 0
      tracim/tracim/controllers/user.py
  17. 二進制
      tracim/tracim/i18n/fr/LC_MESSAGES/tracim.mo
  18. 1 1
      tracim/tracim/i18n/fr/LC_MESSAGES/tracim.po
  19. 13 1
      tracim/tracim/lib/auth/internal.py
  20. 17 1
      tracim/tracim/lib/auth/ldap.py
  21. 38 0
      tracim/tracim/lib/calendar.py
  22. 115 4
      tracim/tracim/lib/content.py
  23. 164 5
      tracim/tracim/lib/daemons.py
  24. 1 1
      tracim/tracim/lib/radicale/auth.py
  25. 4 0
      tracim/tracim/lib/radicale/rights.py
  26. 2 1
      tracim/tracim/lib/userworkspace.py
  27. 49 0
      tracim/tracim/lib/webdav/README.md
  28. 142 0
      tracim/tracim/lib/webdav/__init__.py
  29. 291 0
      tracim/tracim/lib/webdav/design.py
  30. 275 0
      tracim/tracim/lib/webdav/lock_storage.py
  31. 313 0
      tracim/tracim/lib/webdav/sql_dav_provider.py
  32. 48 0
      tracim/tracim/lib/webdav/sql_domain_controller.py
  33. 27 0
      tracim/tracim/lib/webdav/sql_model.py
  34. 1089 0
      tracim/tracim/lib/webdav/sql_resources.py
  35. 100 0
      tracim/tracim/lib/webdav/style.css
  36. 145 0
      tracim/tracim/lib/webdav/tracim_http_authenticator.py
  37. 3 0
      tracim/tracim/lib/workspace.py
  38. 46 0
      tracim/tracim/model/auth.py
  39. 42 27
      tracim/tracim/model/data.py
  40. 2 2
      tracim/tracim/model/serializers.py
  41. 10 0
      tracim/tracim/public/assets/css/dashboard.css
  42. 35 0
      tracim/tracim/public/caldavzap/.htaccess
  43. 24 0
      tracim/tracim/public/caldavzap/auth/.htaccess
  44. 41 0
      tracim/tracim/public/caldavzap/auth/common.inc
  45. 58 0
      tracim/tracim/public/caldavzap/auth/config.inc
  46. 14 0
      tracim/tracim/public/caldavzap/auth/cross_domain.inc
  47. 85 0
      tracim/tracim/public/caldavzap/auth/doc/example_config_response.xml
  48. 7 0
      tracim/tracim/public/caldavzap/auth/doc/readme.txt
  49. 33 0
      tracim/tracim/public/caldavzap/auth/index.php
  50. 58 0
      tracim/tracim/public/caldavzap/auth/plugins/generic.inc
  51. 12 0
      tracim/tracim/public/caldavzap/auth/plugins/generic_conf.inc
  52. 37 0
      tracim/tracim/public/caldavzap/auth/plugins/ldap.inc
  53. 12 0
      tracim/tracim/public/caldavzap/auth/plugins/ldap_conf.inc
  54. 150 0
      tracim/tracim/public/caldavzap/cache.manifest
  55. 79 0
      tracim/tracim/public/caldavzap/cache_handler.js
  56. 5 0
      tracim/tracim/public/caldavzap/cache_update.sh
  57. 294 0
      tracim/tracim/public/caldavzap/changelog.txt
  58. 628 0
      tracim/tracim/public/caldavzap/common.js
  59. 931 0
      tracim/tracim/public/caldavzap/config.js
  60. 2715 0
      tracim/tracim/public/caldavzap/css/default.css
  61. 180 0
      tracim/tracim/public/caldavzap/css/default_integration.css
  62. 1464 0
      tracim/tracim/public/caldavzap/css/fullcalendar.css
  63. 203 0
      tracim/tracim/public/caldavzap/css/jquery-ui.custom.css
  64. 553 0
      tracim/tracim/public/caldavzap/css/spectrum.custom.css
  65. 4265 0
      tracim/tracim/public/caldavzap/data_process.js
  66. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Bold-webfont.eot
  67. 7496 0
      tracim/tracim/public/caldavzap/fonts/Roboto-Bold-webfont.svg
  68. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Bold-webfont.ttf
  69. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Bold-webfont.woff
  70. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-BoldItalic-webfont.eot
  71. 8652 0
      tracim/tracim/public/caldavzap/fonts/Roboto-BoldItalic-webfont.svg
  72. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-BoldItalic-webfont.ttf
  73. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-BoldItalic-webfont.woff
  74. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Italic-webfont.eot
  75. 8164 0
      tracim/tracim/public/caldavzap/fonts/Roboto-Italic-webfont.svg
  76. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Italic-webfont.ttf
  77. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Italic-webfont.woff
  78. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Light-webfont.eot
  79. 8162 0
      tracim/tracim/public/caldavzap/fonts/Roboto-Light-webfont.svg
  80. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Light-webfont.ttf
  81. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Light-webfont.woff
  82. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-LightItalic-webfont.eot
  83. 8162 0
      tracim/tracim/public/caldavzap/fonts/Roboto-LightItalic-webfont.svg
  84. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-LightItalic-webfont.ttf
  85. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-LightItalic-webfont.woff
  86. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Medium-webfont.eot
  87. 7496 0
      tracim/tracim/public/caldavzap/fonts/Roboto-Medium-webfont.svg
  88. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Medium-webfont.ttf
  89. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Medium-webfont.woff
  90. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-MediumItalic-webfont.eot
  91. 8652 0
      tracim/tracim/public/caldavzap/fonts/Roboto-MediumItalic-webfont.svg
  92. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-MediumItalic-webfont.ttf
  93. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-MediumItalic-webfont.woff
  94. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Regular-webfont.eot
  95. 7606 0
      tracim/tracim/public/caldavzap/fonts/Roboto-Regular-webfont.svg
  96. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Regular-webfont.ttf
  97. 二進制
      tracim/tracim/public/caldavzap/fonts/Roboto-Regular-webfont.woff
  98. 202 0
      tracim/tracim/public/caldavzap/fonts/license.txt
  99. 3307 0
      tracim/tracim/public/caldavzap/forms.js
  100. 0 0
      tracim/tracim/public/caldavzap/images/add_cal.svg

+ 1 - 0
.gitignore 查看文件

@@ -60,5 +60,6 @@ tracim/data/
60 60
 # Site-local config file
61 61
 development.ini
62 62
 track.js
63
+wsgidav.conf
63 64
 # Temporary files
64 65
 *~

+ 4 - 0
.travis.yml 查看文件

@@ -5,6 +5,7 @@ python: "3.4"
5 5
 env:
6 6
   - DB=postgres
7 7
   - DB=mysql
8
+  - DB=sqlite
8 9
 
9 10
 addons:
10 11
   postgresql: "9.3"
@@ -27,6 +28,9 @@ before_script:
27 28
   - sh -c "if [ '$DB' = 'mysql' ]; then cd ${TRAVIS_BUILD_DIR}/tracim && sed -i \"s/<replace_database_uri_here>/mysql+oursql:\/\/root@localhost\/tracim_test/\" development.ini; fi"
28 29
   - sh -c "if [ '$DB' = 'mysql' ]; then pip install https://launchpad.net/oursql/py3k/py3k-0.9.4/+download/oursql-0.9.4.zip; fi"
29 30
 
31
+  - sh -c "if [ '$DB' = 'sqlite' ]; then cd ${TRAVIS_BUILD_DIR}/tracim && sed -i \"s/\(sqlalchemy.url *= *\).*/\sqlite:\/\/\/tracim_test.sqlite/\" test.ini; fi"
32
+  - sh -c "if [ '$DB' = 'sqlite' ]; then cd ${TRAVIS_BUILD_DIR}/tracim && sed -i \"s/<replace_database_uri_here>/sqlite:\/\/\/tracim.sqlite/\" development.ini; fi"
33
+
30 34
   - cd ${TRAVIS_BUILD_DIR}/tracim && gearbox setup-app --debug
31 35
 
32 36
 # command to run tests

+ 451 - 400
README.md 查看文件

@@ -1,400 +1,451 @@
1
-[![Build Status](https://travis-ci.org/tracim/tracim.svg?branch=master)](https://travis-ci.org/tracim/tracim) [![Coverage Status](https://img.shields.io/coveralls/tracim/tracim.svg)](https://coveralls.io/r/tracim/tracim) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/tracim/tracim/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/tracim/tracim/?branch=master)
2
-
3
-# Tracim - Introduction #
4
-
5
-Tracim is a collaborative software designed to allow people to share and work on various data and document types.
6
-
7
-If you hesitate to install a wiki, a forum or a file management software, stop hesitating and install Tracim.
8
-
9
-With Tracim, you manage in the same place:
10
-
11
-- forum-like threads,
12
-- files and automatic versionning,
13
-- wiki-like pages for online information,
14
-
15
-All data offers:
16
-
17
-- information status: open / resolved / cancelled / deprecated
18
-- native versionning
19
-- comment threads making tracim knowledge-growth ready.
20
-
21
-Join Tracim community : http://tracim.org
22
-
23
-## Use-cases ##
24
-
25
-### Collaborate with clients ###
26
-
27
-Share information with your clients.
28
-
29
-In the same place you will be able to share trouble-shooting threads, files and general information. You can define who the information is shared with.
30
-
31
-Example: share the documentation with all your users, run a forum open to your clients, another forum for your collaborators and share troubleshooting threads with each of your clients in a private workspace.
32
-
33
-### Run a community of experts or passionate people ###
34
-
35
-Collaborate and share experience and stimulate knowledge growth.
36
-
37
-In a unique place, you centralize files and threads, and raw information too. Every collaborator can update the information status.
38
-Stop worrying about information loss: the traceability is at the hearth of Tracim.
39
-
40
-The newcomers knowledge growth is easy because all information has a status and full history.
41
-You get the status of information and know how it got there.
42
-
43
-### Work on quality-driven projects ###
44
-
45
-In quality-driven projects like research and development, knowledge and quality are more important that task ownership and deadlines.
46
-
47
-With Tracim, you centralize information, you can stay in touch by configuring your email notifications and work on several projects.
48
-
49
-### Manage documents and files ###
50
-
51
-Traceability and versionning are very important for high-quality processes. Unfortunately, specialized software are hard to setup and to use.
52
-
53
-Let's try Tracim ! You define access-control for each workspace and store documents and file there. Users can't delete information: everything is versionned and never deleted.
54
-
55
-The user interface is easy to use: it's based on the well-known folders and files explorer paradigm.
56
-
57
-----
58
-
59
-# Tracim - the software #
60
-
61
-## Licence ##
62
-
63
-Tracim is licensed under the terms of the 
64
-[GNU Affero General Public License](http://www.gnu.org/licenses/agpl.txt) as published by the [Free Software Foundation](http://www.fsf.org/).
65
-
66
-## Technical information ##
67
-
68
-Tracim is a web application:
69
-
70
-* developed with python >=3.4.
71
-* based on the [TurboGears](http://www.turbogears.org/) web framework.
72
-* relying on [PostgreSQL](http://www.postgresql.org/) as the storage engine.
73
-
74
-The user interface is based on the following resources and technologies:
75
-
76
-* [Mako](http://www.makotemplates.org/) templating engine (server-side)
77
-* [Bootstrap 3](http://getbootstrap.com/)
78
-* [jQuery](http://wwwjquery.corm)
79
-* Icons are taken from [Tango Icons](http://tango.freedesktop.org/) and [Font Awesome](http://fortawesome.github.io/Font-Awesome/)
80
-* The design is based on the [Bootstrap dashboard example](http://getbootstrap.com/examples/dashboard/) and uses some images from [Start Boostrap free templates](http://startbootstrap.com/)
81
-
82
-
83
-
84
-It runs on [Debian GNU/Linux](http://www.debian.org/), it should work out-of-the-box on [Ubuntu](http://www.ubuntu.com/) and also on other GNU/Linux distributions.
85
-
86
-Hopefully it works on BSD and Windows OSes (but this has not been tested yet)
87
-
88
-----
89
-
90
-# Use it (or give it a try) #
91
-
92
-## Online Demo ##
93
-
94
-The easiest way to test Tracim is to test it through the online demo:
95
-
96
-* [http://demo.tracim.fr](http://demo.tracim.fr)
97
-* login as admin: admin@admin.admin
98
-* password: admin@admin.admin
99
-
100
-## Ask for a dedicated instance ##
101
-
102
-If you want your own dedicated instance but do not want to manage it by yourself, let's contact me at damien.accorsi@free.fr
103
-
104
-## Install Tracim on your server ##
105
-
106
-Following the installation documentation below, you'll be able to run your own instance on your server.
107
-
108
-----
109
-
110
-# Installation #
111
-
112
-## Dependencies ##
113
-
114
-_Note: the following information is for Debian. For other OS, adapt the package names._
115
-
116
-You'll need to install the following packages:
117
-
118
-    apt-get install realpath python3 python-virtualenv python3-dev python-pip build-essential postgresql-server-dev-all libxml2-dev libxslt1-dev python-lxml
119
-
120
-If you work on a local database, then you also need to install PostgreSQL:
121
-
122
-    apt-get install postgresql postgresql-client
123
-
124
-## Installation ##
125
-
126
-### Get the source ###
127
-
128
-Get the sources from Bitbucket:
129
-
130
-    git clone https://github.com/tracim/tracim.git
131
-    cd tracim/
132
-
133
-*Note: Now everything is documented to be executed from the tracim directory newly created.*
134
-
135
-
136
-### Setting-up python virtualenv ###
137
-
138
-_Reminder : Tracim is developped and tested using python3.4._
139
-
140
-Tracim uses virtualenv as deployment environment. This ensure that there will be no 
141
-conflict between system-wide python installation and Tracim required ones.
142
-
143
-    virtualenv -p /usr/bin/python3 tg2env
144
-    source tg2env/bin/activate
145
-    cd tracim && python setup.py develop && cd -
146
-    pip install -r install/requirements.txt
147
-
148
-## Database Setup ##
149
-
150
-### Minimalist introduction to PostgreSQL ###
151
-
152
-If you already use/know PostgreSQL, you can directly go to *Test the database access*.
153
-
154
-#### Allowing local connections on PostgreSQL ####
155
-
156
-PostgreSQL stores connections ahtorization in *pg\_hba.conf*
157
-
158
-Edit the pg_hba.conf file and check that connectionx from 127.0.0.1 are allowed using user/password. You should find the following line in the file:
159
-
160
-    # IPv4 local connections:
161
-    host    all             all             127.0.0.1/32            md5
162
-
163
-Note: on Debian, the *pg\_hba.conf* file is found at */etc/postgresql/9.1/main/pg\_hba.conf*
164
-
165
-If you changed the file, reload PostgreSQL:
166
-
167
-    service postgresql reload
168
-
169
-#### Creating a user and associated database ####
170
-
171
-You need a database and associated user/password.
172
-
173
-Tracim comes with a tool that will make this step easy : pgtool.
174
-
175
-    ~/tracim$ ./bin/pgtool help
176
-
177
-login as *postgres* user and run the follwoing commands (which are self explanatory)
178
-
179
-    ./bin/pgtool create_user tracimuser tracimpassword
180
-    ./bin/pgtool create_database tracimdb
181
-    ./bin/pgtool grant_all_privileges tracimdb tracimuser
182
-
183
-Notes :
184
-
185
-* in order to login as postgres user, su as root (with your password) then su postgres.
186
-* pgtool also offers options to delete users / databases. Run *./bin/pgtool help* for more information
187
-
188
-#### Test the database access ####
189
-
190
-So, now you have a database and an associated user/password.
191
-
192
-A good habit is to test things before to use them, that's why we want to test the database access now. This is easily done with tracim pgtool :
193
-
194
-    ./bin/pgtool test_connection tracimdb tracimuser tracimpassword 127.0.0.1
195
-
196
-The result is similar to the following :
197
-
198
-    PG # CONNECT TO DATABASE
199
-    ------------------------
200
-    server:     127.0.0.1
201
-    database:   tracimdb
202
-    username:   bibi
203
-
204
-                  now              
205
-    -------------------------------
206
-     2014-11-10 09:40:23.306199+01
207
-    (1 row)
208
-
209
-In case of failure, you would get something like this:
210
-
211
-    PG # CONNECT TO DATABASE
212
-    ------------------------
213
-    server:     127.0.0.1
214
-    database:   tracimdb
215
-    username:   bibi
216
-
217
-    psql: FATAL:  password authentication failed for user "bibi"
218
-    FATAL:  password authentication failed for user "bibi"
219
-    ERRROR
220
-
221
-In this case, delete the user and database you previously created (using pgtool) and do it again. Do not forget to run the grant_all_rights command!
222
-
223
-## Configuration ##
224
-
225
-At this point, you have :
226
-
227
-* an installation of Tracim with its dedicated python3-ready virtualenv
228
-* a PostgreSQL server and dedicated database
229
-
230
-What you have to do now is to configure the application and to initialize the database content.
231
-
232
-### Create configuration ###
233
-
234
-    cp tracim/development.ini.base tracim/development.ini
235
-
236
-You can now edit the file and setup required files. Here are the main ones:
237
-
238
-#### Database access ####
239
-
240
-Configure database in the development.ini file. This is defined as sqlalchemy.url
241
-and the default value is below:
242
-
243
-    sqlalchemy.url = postgresql://tracimuser:tracimpassword@127.0.0.1:5432/tracimdb?client_encoding=utf8
244
-
245
-#### Listening port
246
-
247
-Default configuration is to listen on port 8080. If you want to adapt this to your environment, edit the .ini file and setup the port you want:
248
-
249
-    port = 8080
250
-
251
-#### Interface language
252
-
253
-The default language is English. You can change it to French by uncommenting the following line in the .ini file:
254
-
255
-    lang = fr
256
-
257
-#### SMTP parameters for resetpassword and notifications
258
-
259
-for some reason, you have to configure SMTP parameters for rest password process and SMTP parameters for notifications in separate places.
260
-
261
-The reset password related parameters are the follwoing ones :
262
-
263
-    resetpassword.email_sender = tracim@mycompany.com
264
-    resetpassword.smtp_host = smtp.mycompany.com
265
-    resetpassword.smtp_port = 25
266
-    resetpassword.smtp_login = username
267
-    resetpassword.smtp_passwd = password
268
-
269
-The main parameters for notifications are the following ones:
270
-
271
-    email.notification.activated = true
272
-    email.notification.from = Tracim Notification <tracim@tmycompany.com>
273
-    email.notification.smtp.server = smtp.mycompany.com
274
-    email.notification.smtp.port = 25
275
-    email.notification.smtp.user = username
276
-    email.notification.smtp.password = password
277
-
278
-#### Website ####
279
-
280
-You must define general parameters like the base_url and the website title which are required for home page and email notification links
281
-
282
-    website.title = My Company Intranet
283
-    website.base_url = http://intranet.mycompany.com:8080
284
-
285
-#### LDAP ####
286
-
287
-To use LDAP authentication, set ``auth_type`` parameter to "ldap":
288
-
289
-    auth_type = ldap
290
-
291
-Then add LDAP parameters
292
-
293
-    # LDAP server address
294
-    ldap_url = ldap://localhost:389
295
-
296
-    # Base dn to make queries
297
-    ldap_base_dn = dc=directory,dc=fsf,dc=org
298
-
299
-    # Bind dn to identify the search
300
-    ldap_bind_dn = cn=admin,dc=directory,dc=fsf,dc=org
301
-
302
-    # The bind password
303
-    ldap_bind_pass = toor
304
-
305
-    # Attribute name of user record who contain user login (email)
306
-    ldap_ldap_naming_attribute = uid
307
-
308
-    # Matching between ldap attribute and ldap user field (ldap_attr1=user_field1,ldap_attr2=user_field2,...)
309
-    ldap_user_attributes = mail=email
310
-
311
-    # TLS usage to communicate with your LDAP server
312
-    ldap_tls = False
313
-
314
-    # If True, LDAP own tracim group managment (not available for now!)
315
-    ldap_group_enabled = False
316
-
317
-You may need an administrator account to manage Tracim. Use the following command (from ``/install/dir/of/tracim/tracim``):
318
-
319
-```
320
-gearbox user create -l admin-email@domain.com -g managers -g administrators
321
-```
322
-
323
-Keep in mind ``admin-email@domain.com`` must match with LDAP user.
324
-
325
-#### Other parameters  ####
326
-
327
-There are other parameters which may be of some interest for you. For example, you can:
328
-
329
-* include a JS tracker like Piwik or Google Analytics,
330
-* define your own notification email subject
331
-* personalize notification email
332
-* personalize home page (background image, title color...)
333
-* ...
334
-
335
-### database schema ###
336
-
337
-The last step before to run the application is to initialize the database schema. This is done through the following command:
338
-
339
-    source tg2env/bin/activate
340
-    cd tracim && gearbox setup-app && cd -
341
-
342
-## Running the server ##
343
-
344
-### Running Tracim in standalone mode ###
345
-
346
-Now you can run the standalone server:
347
-
348
-    ./bin/run.sh
349
-    
350
-Which should result in something like this:
351
-
352
-    13:53:49,982 INFO  [gearbox] Starting subprocess with file monitor
353
-    13:53:50,646 WARNI [py.warnings] /tmp/tracim/protov1/tg2env/lib/python3.2/site-packages/tw2/core/validation.py:12: ImportWarning: Not importing directory '/tmp/tracim/protov1/tg2env/lib/python3.2/site-packages/tw2/core/i18n': missing __init__.py
354
-      from .i18n import _
355
-    
356
-    13:53:50,862 INFO  [gearbox] Starting server in PID 11174.
357
-    Starting HTTP server on http://0.0.0.0:8080
358
-    
359
-You can now enter the application at [http://localhost:8080](http://localhost:8080) and login:
360
-
361
-* user : admin@admin.admin
362
-* password : admin@admin.admin
363
-    
364
-Enjoy :)
365
-
366
-### Running Tracim through Apache WSGI ###
367
-
368
-#### Dependencies ####
369
-
370
-Install dependencies:
371
-
372
-    apt-get install apache2 libapache2-mod-wsgi-py3
373
-
374
-#### WSGI configuration ####
375
-
376
-Example of Apache WSGI configuration. This configuration refers to productionapp.wsgi which is a copy of the file *app.wsgi* available in the repo. (this file has to be updated to match with your environment and installation)
377
-
378
-    <VirtualHost *:80>
379
-        ServerAdmin webmaster@tracim.mycompany.com
380
-        ServerName tracim.mycompany.com
381
-
382
-        WSGIProcessGroup tracim
383
-        WSGIDaemonProcess tracim user=www-data group=adm threads=4 python-path=/opt/traciminstall/tg2env/lib/python3.2/site-packages
384
-        WSGIScriptAlias / /opt/traciminstall/tracim/productionapp.wsgi
385
-
386
-        #Serve static files directly without TurboGears
387
-        Alias /assets     /opt/traciminstall/tracim/tracim/public/assets
388
-        Alias /favicon.ico /opt/traciminstall/tracim/tracim/public/favicon.ico
389
-
390
-        CustomLog /var/log/apache2/demotracim-access.log combined
391
-        ErrorLog /var/log/apache2/demotracim-error.log
392
-        LogLevel debug
393
-    </VirtualHost>
394
-
395
-# Support and Community #
396
-
397
-Building the community is a work in progress.
398
-
399
-Need help ? Do not hesitate to contact me : damien.accorsi@free.fr
400
-
1
+[![Build Status](https://travis-ci.org/tracim/tracim.svg?branch=master)](https://travis-ci.org/tracim/tracim) [![Coverage Status](https://img.shields.io/coveralls/tracim/tracim.svg)](https://coveralls.io/r/tracim/tracim) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/tracim/tracim/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/tracim/tracim/?branch=master)
2
+
3
+# Tracim - Introduction #
4
+
5
+Tracim is a collaborative software designed to allow people to share and work on various data and document types.
6
+
7
+If you hesitate to install a wiki, a forum or a file management software, stop hesitating and install Tracim.
8
+
9
+With Tracim, you manage in the same place:
10
+
11
+- forum-like threads,
12
+- files and automatic versionning,
13
+- wiki-like pages for online information,
14
+
15
+All data offers:
16
+
17
+- information status: open / resolved / cancelled / deprecated
18
+- native versionning
19
+- comment threads making tracim knowledge-growth ready.
20
+
21
+Join Tracim community : http://tracim.org
22
+
23
+## Use-cases ##
24
+
25
+### Collaborate with clients ###
26
+
27
+Share information with your clients.
28
+
29
+In the same place you will be able to share trouble-shooting threads, files and general information. You can define who the information is shared with.
30
+
31
+Example: share the documentation with all your users, run a forum open to your clients, another forum for your collaborators and share troubleshooting threads with each of your clients in a private workspace.
32
+
33
+### Run a community of experts or passionate people ###
34
+
35
+Collaborate and share experience and stimulate knowledge growth.
36
+
37
+In a unique place, you centralize files and threads, and raw information too. Every collaborator can update the information status.
38
+Stop worrying about information loss: the traceability is at the hearth of Tracim.
39
+
40
+The newcomers knowledge growth is easy because all information has a status and full history.
41
+You get the status of information and know how it got there.
42
+
43
+### Work on quality-driven projects ###
44
+
45
+In quality-driven projects like research and development, knowledge and quality are more important that task ownership and deadlines.
46
+
47
+With Tracim, you centralize information, you can stay in touch by configuring your email notifications and work on several projects.
48
+
49
+### Manage documents and files ###
50
+
51
+Traceability and versionning are very important for high-quality processes. Unfortunately, specialized software are hard to setup and to use.
52
+
53
+Let's try Tracim ! You define access-control for each workspace and store documents and file there. Users can't delete information: everything is versionned and never deleted.
54
+
55
+The user interface is easy to use: it's based on the well-known folders and files explorer paradigm.
56
+
57
+----
58
+
59
+# Tracim - the software #
60
+
61
+## Licence ##
62
+
63
+Tracim is licensed under the terms of the
64
+[GNU Affero General Public License](http://www.gnu.org/licenses/agpl.txt) as published by the [Free Software Foundation](http://www.fsf.org/).
65
+
66
+## Technical information ##
67
+
68
+Tracim is a web application:
69
+
70
+* developed with python >=3.4.
71
+* based on the [TurboGears](http://www.turbogears.org/) web framework.
72
+* relying on [PostgreSQL](http://www.postgresql.org/) or [MySQL](https://www.mysql.fr/) or [sqlite](https://www.sqlite.org/) as the storage engine.
73
+
74
+The user interface is based on the following resources and technologies:
75
+
76
+* [Mako](http://www.makotemplates.org/) templating engine (server-side)
77
+* [Bootstrap 3](http://getbootstrap.com/)
78
+* [jQuery](http://wwwjquery.corm)
79
+* Icons are taken from [Tango Icons](http://tango.freedesktop.org/) and [Font Awesome](http://fortawesome.github.io/Font-Awesome/)
80
+* The design is based on the [Bootstrap dashboard example](http://getbootstrap.com/examples/dashboard/) and uses some images from [Start Boostrap free templates](http://startbootstrap.com/)
81
+
82
+It runs on [Debian GNU/Linux](http://www.debian.org/), it should work out-of-the-box on [Ubuntu](http://www.ubuntu.com/) and also on other GNU/Linux distributions.
83
+
84
+Hopefully it works on BSD and Windows OSes (but this has not been tested yet).
85
+
86
+----
87
+
88
+# Use it (or give it a try) #
89
+
90
+## Online Demo ##
91
+
92
+The easiest way to test Tracim is to test it through the online demo:
93
+
94
+* [http://demo.tracim.fr](http://demo.tracim.fr)
95
+* login as admin: admin@admin.admin
96
+* password: admin@admin.admin
97
+
98
+## Ask for a dedicated instance ##
99
+
100
+If you want your own dedicated instance but do not want to manage it by yourself, let's contact me at damien.accorsi@free.fr
101
+
102
+## Install Tracim on your server ##
103
+
104
+Following the installation documentation below, you'll be able to run your own instance on your server.
105
+
106
+----
107
+
108
+# Installation #
109
+
110
+## Dependencies ##
111
+
112
+_Note: the following information is for Debian. For other OS, adapt the package names._
113
+
114
+You'll need to install the following packages on your Operating System:
115
+
116
+    apt-get install git realpath python3 python-virtualenv python3-dev python-pip build-essential libxml2-dev libxslt1-dev python-lxml
117
+
118
+## Database ##
119
+
120
+If you want use PostgreSQL as database engine:
121
+
122
+    apt-get install postgresql-server-dev-all postgresql postgresql-client
123
+
124
+Or if you want to use MySQL as database engine
125
+
126
+    apt-get install mysql-server mysql-client libmysqlclient-dev
127
+
128
+Or if you want to use SQLite as database engine
129
+
130
+    apt-get install sqlite3
131
+
132
+## Installation ##
133
+
134
+### Get the source ###
135
+
136
+Get the sources from github with git:
137
+
138
+    git clone https://github.com/tracim/tracim.git
139
+    cd tracim/
140
+
141
+*Note: Now everything is documented to be executed from the tracim directory newly created.*
142
+
143
+### Setting-up python virtualenv ###
144
+
145
+_Reminder : Tracim is developed and tested using python3.4._
146
+
147
+We strongly recommend to use virtualenv as deployment environment. This ensure that there will be no conflict between system-wide python installation and Tracim required ones. To Create the virtual environment:
148
+
149
+    virtualenv -p /usr/bin/python3.4 tg2env
150
+
151
+And to activate it in your terminal session (**all tracim command execution must be executed under this virtual environment**)):
152
+
153
+    source tg2env/bin/activate
154
+
155
+To install tracim and it's dependencies:
156
+
157
+    cd tracim && python setup.py develop && cd -
158
+    pip install -r install/requirements.txt
159
+
160
+**Note**: If you want to use MySQL database, please refer to Configuration/database schema note to install required package.
161
+
162
+## Database Setup ##
163
+
164
+### Minimalist introduction to PostgreSQL ###
165
+
166
+If you already use/know PostgreSQL, you can directly go to *Test the database access*.
167
+
168
+#### Allowing local connections on PostgreSQL ####
169
+
170
+PostgreSQL stores connections ahtorization in *pg\_hba.conf*
171
+
172
+Edit the pg_hba.conf file and check that connectionx from 127.0.0.1 are allowed using user/password. You should find the following line in the file:
173
+
174
+    # IPv4 local connections:
175
+    host    all             all             127.0.0.1/32            md5
176
+
177
+Note: on Debian, the *pg\_hba.conf* file is found at */etc/postgresql/9.1/main/pg\_hba.conf*
178
+
179
+If you changed the file, reload PostgreSQL:
180
+
181
+    service postgresql reload
182
+
183
+#### Creating a user and associated database ####
184
+
185
+You need a database and associated user/password.
186
+
187
+Tracim comes with a tool that will make this step easy : pgtool.
188
+
189
+    ~/tracim$ ./bin/pgtool help
190
+
191
+login as *postgres* user and run the follwoing commands (which are self explanatory)
192
+
193
+    ./bin/pgtool create_user tracimuser tracimpassword
194
+    ./bin/pgtool create_database tracimdb
195
+    ./bin/pgtool grant_all_privileges tracimdb tracimuser
196
+
197
+Notes :
198
+
199
+* in order to login as postgres user, su as root (with your password) then su postgres.
200
+* pgtool also offers options to delete users / databases. Run *./bin/pgtool help* for more information
201
+
202
+#### Test the database access ####
203
+
204
+So, now you have a database and an associated user/password.
205
+
206
+A good habit is to test things before to use them, that's why we want to test the database access now. This is easily done with tracim pgtool :
207
+
208
+    ./bin/pgtool test_connection tracimdb tracimuser tracimpassword 127.0.0.1
209
+
210
+The result is similar to the following :
211
+
212
+    PG # CONNECT TO DATABASE
213
+    ------------------------
214
+    server:     127.0.0.1
215
+    database:   tracimdb
216
+    username:   bibi
217
+
218
+                  now
219
+    -------------------------------
220
+     2014-11-10 09:40:23.306199+01
221
+    (1 row)
222
+
223
+In case of failure, you would get something like this:
224
+
225
+    PG # CONNECT TO DATABASE
226
+    ------------------------
227
+    server:     127.0.0.1
228
+    database:   tracimdb
229
+    username:   bibi
230
+
231
+    psql: FATAL:  password authentication failed for user "bibi"
232
+    FATAL:  password authentication failed for user "bibi"
233
+    ERRROR
234
+
235
+In this case, delete the user and database you previously created (using pgtool) and do it again. Do not forget to run the grant_all_rights command!
236
+
237
+### Minimalist introduction to MySQL ###
238
+
239
+## Create database ##
240
+
241
+Connect to mysql with root user (password has been set at "Installation" -> "Dependencies" chapter, when installing package)
242
+
243
+    mysql -u root -p
244
+
245
+Create a database with following command:
246
+
247
+    CREATE DATABASE tracimdb;
248
+
249
+Create a user with following command:
250
+
251
+    CREATE USER 'tracimuser'@'localhost' IDENTIFIED BY 'tracimpassword';
252
+
253
+And allow him to manipulate created database with following command:
254
+
255
+    GRANT ALL PRIVILEGES ON tracimdb . * TO 'tracimuser'@'localhost';
256
+
257
+Then flush privileges:
258
+
259
+    FLUSH PRIVILEGES;
260
+
261
+You can now quit mysql prompt:
262
+
263
+    \q
264
+
265
+## Configuration ##
266
+
267
+At this point, you have :
268
+
269
+* an installation of Tracim with its dedicated python3-ready virtualenv
270
+* a PostgreSQL/MySQL server and dedicated database (if you don't use sqlite)
271
+
272
+What you have to do now is to configure the application and to initialize the database content.
273
+
274
+### Create configuration ###
275
+
276
+    cp tracim/development.ini.base tracim/development.ini
277
+
278
+You can now edit the file and setup required files. Here are the main ones:
279
+
280
+#### Database access ####
281
+
282
+Configure database in the development.ini file. This is defined as sqlalchemy.url. There is an example value for PostgreSQL below:
283
+
284
+    sqlalchemy.url = postgresql://tracimuser:tracimpassword@127.0.0.1:5432/tracimdb?client_encoding=utf8
285
+
286
+There is an example value for MySQL below (please refer to Configuration/database schema note to install required package):
287
+
288
+    sqlalchemy.url = mysql+oursql://tracimuser:tracimpassword@127.0.0.1/tracimdb
289
+
290
+There is an example value for SQLite below :
291
+
292
+    sqlalchemy.url = sqlite:///tracimdb.sqlite
293
+
294
+#### Listening port
295
+
296
+Default configuration is to listen on port 8080. If you want to adapt this to your environment, edit the .ini file and setup the port you want:
297
+
298
+    port = 8080
299
+
300
+#### Interface language
301
+
302
+The default language is English. You can change it to French by uncommenting the following line in the .ini file:
303
+
304
+    lang = fr
305
+
306
+#### SMTP parameters for resetpassword and notifications
307
+
308
+for technical reason, you have to configure SMTP parameters for rest password process and SMTP parameters for notifications in separate places.
309
+
310
+The reset password related parameters are the follwoing ones :
311
+
312
+    resetpassword.email_sender = tracim@mycompany.com
313
+    resetpassword.smtp_host = smtp.mycompany.com
314
+    resetpassword.smtp_port = 25
315
+    resetpassword.smtp_login = username
316
+    resetpassword.smtp_passwd = password
317
+
318
+The main parameters for notifications are the following ones:
319
+
320
+    email.notification.activated = true
321
+    email.notification.from = Tracim Notification <tracim@tmycompany.com>
322
+    email.notification.smtp.server = smtp.mycompany.com
323
+    email.notification.smtp.port = 25
324
+    email.notification.smtp.user = username
325
+    email.notification.smtp.password = password
326
+
327
+#### Website ####
328
+
329
+You must define general parameters like the base_url and the website title which are required for home page and email notification links
330
+
331
+    website.title = My Company Intranet
332
+    website.base_url = http://intranet.mycompany.com:8080
333
+
334
+#### LDAP ####
335
+
336
+To use LDAP authentication, set ``auth_type`` parameter to "ldap":
337
+
338
+    auth_type = ldap
339
+
340
+Then add LDAP parameters
341
+
342
+    # LDAP server address
343
+    ldap_url = ldap://localhost:389
344
+
345
+    # Base dn to make queries
346
+    ldap_base_dn = dc=directory,dc=fsf,dc=org
347
+
348
+    # Bind dn to identify the search
349
+    ldap_bind_dn = cn=admin,dc=directory,dc=fsf,dc=org
350
+
351
+    # The bind password
352
+    ldap_bind_pass = toor
353
+
354
+    # Attribute name of user record who contain user login (email)
355
+    ldap_ldap_naming_attribute = uid
356
+
357
+    # Matching between ldap attribute and ldap user field (ldap_attr1=user_field1,ldap_attr2=user_field2,...)
358
+    ldap_user_attributes = mail=email
359
+
360
+    # TLS usage to communicate with your LDAP server
361
+    ldap_tls = False
362
+
363
+    # If True, LDAP own tracim group managment (not available for now!)
364
+    ldap_group_enabled = False
365
+
366
+You may need an administrator account to manage Tracim. Use the following command (from ``/install/dir/of/tracim/tracim``):
367
+
368
+    gearbox user create -l admin@admin.admin -p admin@admin.admin -g managers -g administrators
369
+
370
+Keep in mind ``admin-email@domain.com`` must match with LDAP user.
371
+
372
+#### Other parameters  ####
373
+
374
+There are other parameters which may be of some interest for you. For example, you can:
375
+
376
+* include a JS tracker like Piwik or Google Analytics,
377
+* define your own notification email subject
378
+* personalize notification email
379
+* personalize home page (background image, title color...)
380
+* ...
381
+
382
+### database schema ###
383
+
384
+The last step before to run the application is to initialize the database schema. This is done through the following command:
385
+
386
+**Note**: If you want to use MySQL database, please install this pip package: ```pip install https://launchpad.net/oursql/py3k/py3k-0.9.4/+download/oursql-0.9.4.zip```
387
+
388
+    cd tracim && gearbox setup-app && cd -
389
+
390
+## Running the server ##
391
+
392
+### Running Tracim in standalone mode ###
393
+
394
+Now you can run the standalone server:
395
+
396
+    ./bin/run.sh
397
+
398
+Which should result in something like this:
399
+
400
+    13:53:49,982 INFO  [gearbox] Starting subprocess with file monitor
401
+    13:53:50,646 WARNI [py.warnings] /tmp/tracim/protov1/tg2env/lib/python3.2/site-packages/tw2/core/validation.py:12: ImportWarning: Not importing directory '/tmp/tracim/protov1/tg2env/lib/python3.2/site-packages/tw2/core/i18n': missing __init__.py
402
+      from .i18n import _
403
+
404
+    13:53:50,862 INFO  [gearbox] Starting server in PID 11174.
405
+    Starting HTTP server on http://0.0.0.0:8080
406
+
407
+You can now enter the application at [http://localhost:8080](http://localhost:8080) and login with admin user.
408
+
409
+ * user : admin@admin.admin
410
+ * password : admin@admin.admin
411
+
412
+If admin user not created yet, execute following command:
413
+
414
+    gearbox user create -l admin@admin.admin -p admin@admin.admin -g managers -g administrators
415
+
416
+Enjoy :)
417
+
418
+### Running Tracim through Apache WSGI ###
419
+
420
+#### Dependencies ####
421
+
422
+Install dependencies:
423
+
424
+    apt-get install apache2 libapache2-mod-wsgi-py3
425
+
426
+#### WSGI configuration ####
427
+
428
+Example of Apache WSGI configuration. This configuration refers to productionapp.wsgi which is a copy of the file *app.wsgi* available in the repo. (this file has to be updated to match with your environment and installation)
429
+
430
+    <VirtualHost *:80>
431
+        ServerAdmin webmaster@tracim.mycompany.com
432
+        ServerName tracim.mycompany.com
433
+
434
+        WSGIProcessGroup tracim
435
+        WSGIDaemonProcess tracim user=www-data group=adm threads=4 python-path=/opt/traciminstall/tg2env/lib/python3.2/site-packages
436
+        WSGIScriptAlias / /opt/traciminstall/tracim/productionapp.wsgi
437
+
438
+        #Serve static files directly without TurboGears
439
+        Alias /assets     /opt/traciminstall/tracim/tracim/public/assets
440
+        Alias /favicon.ico /opt/traciminstall/tracim/tracim/public/favicon.ico
441
+
442
+        CustomLog /var/log/apache2/demotracim-access.log combined
443
+        ErrorLog /var/log/apache2/demotracim-error.log
444
+        LogLevel debug
445
+    </VirtualHost>
446
+
447
+# Support and Community #
448
+
449
+Building the community is a work in progress.
450
+
451
+Need help ? Do not hesitate to contact me : damien.accorsi@free.fr

+ 3 - 1
install/requirements.txt 查看文件

@@ -1,3 +1,4 @@
1
+pytz==2014.7
1 2
 Babel==2.2.0
2 3
 Beaker==1.6.4
3 4
 CherryPy==3.6.0
@@ -38,7 +39,6 @@ pyparsing==2.0.3
38 39
 python-dateutil==2.5.3
39 40
 python-editor==1.0.1
40 41
 python-ldap-test==0.2.1
41
-pytz==2014.7
42 42
 repoze.lru==0.6
43 43
 repoze.who==2.2
44 44
 requests==2.10.0
@@ -59,5 +59,7 @@ unicode-slugify==0.1.3
59 59
 vobject==0.9.2
60 60
 waitress==0.8.9
61 61
 who-ldap==3.1.0
62
+-e git+git@github.com:Nonolost/wsgidav.git@a5df223d5c66c252baa89ec5de8073f9f1494a8f#egg=wsgidav
62 63
 zope.interface==4.1.3
63 64
 zope.sqlalchemy==0.7.6
65
+

+ 4 - 0
tracim/development.ini.base 查看文件

@@ -67,6 +67,9 @@ auth_type = internal
67 67
 # If True, LDAP own tracim group managment (not available for now!)
68 68
 # ldap_group_enabled = False
69 69
 
70
+# User auth token validity in seconds (used to interfaces like web calendars)
71
+user.auth_token.validity = 604800
72
+
70 73
 #By default session is store in cookies to avoid the overhead
71 74
 #of having to manage a session storage. On production you might
72 75
 #want to switch to a better session storage.
@@ -194,6 +197,7 @@ email.notification.smtp.password = your_smtp_password
194 197
 # radicale.server.ssl = false
195 198
 # radicale.server.filesystem.folder = ~/.config/radicale/collections
196 199
 # radicale.server.allow_origin = *
200
+# radicale.server.realm_message = Tracim Calendar - Password Required
197 201
 ## url can be extended like http://127.0.0.1:5232/calendar
198 202
 ## in this case, you have to create your own proxy behind this url.
199 203
 # radicale.client.base_url = http://127.0.0.1:5232

+ 28 - 0
tracim/migration/versions/b4b8d57b54e5_add_hash_column_for_digest_.py 查看文件

@@ -0,0 +1,28 @@
1
+"""add hash column for digest authentication
2
+
3
+Revision ID: b4b8d57b54e5
4
+Revises: 534c4594ed29
5
+Create Date: 2016-08-11 10:27:28.951506
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = 'b4b8d57b54e5'
11
+down_revision = 'bdb195ed95bb'
12
+
13
+from alembic import op
14
+from sqlalchemy import Column, Unicode, Boolean
15
+
16
+
17
+def upgrade():
18
+    op.add_column('users', Column('webdav_left_digest_response_hash', Unicode(128)))
19
+    op.add_column('content_revisions', Column('is_temporary', Boolean(), unique=False, nullable=True))
20
+    op.execute('''
21
+        UPDATE content_revisions
22
+        SET is_temporary = FALSE
23
+        ''')
24
+    op.alter_column('content_revisions', 'is_temporary', nullable=False)
25
+
26
+def downgrade():
27
+    op.drop_column('users', 'webdav_left_digest_response_hash')
28
+    op.drop_column('content_revisions', 'is_temporary')

+ 28 - 0
tracim/migration/versions/bdb195ed95bb_user_auth_token_columns.py 查看文件

@@ -0,0 +1,28 @@
1
+"""User auth token columns
2
+
3
+Revision ID: bdb195ed95bb
4
+Revises: 534c4594ed29
5
+Create Date: 2016-07-28 15:38:18.889151
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = 'bdb195ed95bb'
11
+down_revision = '534c4594ed29'
12
+
13
+from alembic import op
14
+import sqlalchemy as sa
15
+
16
+
17
+def upgrade():
18
+    ### commands auto generated by Alembic - please adjust! ###
19
+    op.add_column('users', sa.Column('auth_token', sa.Unicode(length=255), nullable=True))
20
+    op.add_column('users', sa.Column('auth_token_created', sa.DateTime(), nullable=True))
21
+    ### end Alembic commands ###
22
+
23
+
24
+def downgrade():
25
+    ### commands auto generated by Alembic - please adjust! ###
26
+    op.drop_column('users', 'auth_token_created')
27
+    op.drop_column('users', 'auth_token')
28
+    ### end Alembic commands ###

+ 1 - 0
tracim/setup.py 查看文件

@@ -42,6 +42,7 @@ install_requires=[
42 42
     "who-ldap==3.1.0",
43 43
     "python-ldap-test==0.2.1",
44 44
     "unicode-slugify==0.1.3",
45
+    "pytz==2014.7",
45 46
     ]
46 47
 
47 48
 setup(

+ 3 - 3
tracim/test.ini 查看文件

@@ -19,7 +19,7 @@ host = 127.0.0.1
19 19
 port = 8080
20 20
 
21 21
 [app:main]
22
-sqlalchemy.url = postgresql://postgres:dummy@127.0.0.1:5432/tracim_test?client_encoding=utf8
22
+sqlalchemy.url = sqlite:///tracim_test.sqlite
23 23
 use = config:development.ini
24 24
 
25 25
 [app:main_without_authn]
@@ -27,7 +27,7 @@ use = main
27 27
 skip_authentication = True
28 28
 
29 29
 [app:ldap]
30
-sqlalchemy.url = postgresql://postgres:dummy@127.0.0.1:5432/tracim_test?client_encoding=utf8
30
+sqlalchemy.url = sqlite:///tracim_test.sqlite
31 31
 auth_type = ldap
32 32
 ldap_url = ldap://localhost:3333
33 33
 ldap_base_dn = dc=directory,dc=fsf,dc=org
@@ -40,7 +40,7 @@ ldap_group_enabled = False
40 40
 use = config:development.ini
41 41
 
42 42
 [app:radicale]
43
-sqlalchemy.url = postgresql://postgres:dummy@127.0.0.1:5432/tracim_test?client_encoding=utf8
43
+sqlalchemy.url = sqlite:///tracim_test.sqlite
44 44
 
45 45
 use = config:development.ini
46 46
 

+ 3 - 1
tracim/tracim/command/__init__.py 查看文件

@@ -58,7 +58,9 @@ class AppContextCommand(BaseCommand):
58 58
         sys.path.insert(0, here_dir)
59 59
 
60 60
         # Load the wsgi app first so that everything is initialized right
61
-        wsgi_app = loadapp(config_name, relative_to=here_dir)
61
+        wsgi_app = loadapp(config_name, relative_to=here_dir, global_conf={
62
+            'disable_daemons': 'true',
63
+        })
62 64
         test_app = TestApp(wsgi_app)
63 65
 
64 66
         # Make available the tg.request and other global variables

+ 26 - 1
tracim/tracim/config/app_cfg.py 查看文件

@@ -31,6 +31,7 @@ from tracim.lib.auth.wrapper import AuthConfigWrapper
31 31
 from tracim.lib.base import logger
32 32
 from tracim.lib.daemons import DaemonsManager
33 33
 from tracim.lib.daemons import RadicaleDaemon
34
+from tracim.lib.daemons import WsgiDavDaemon
34 35
 from tracim.model.data import ActionDescription
35 36
 from tracim.model.data import ContentType
36 37
 
@@ -94,7 +95,13 @@ def start_daemons(manager: DaemonsManager):
94 95
     """
95 96
     Sart Tracim daemons
96 97
     """
98
+    from tg import config
99
+    # Don't start daemons if they are disabled
100
+    if 'disable_daemons' in config and config['disable_daemons']:
101
+        return
102
+
97 103
     manager.run('radicale', RadicaleDaemon)
104
+    manager.run('webdav', WsgiDavDaemon)
98 105
 
99 106
 environment_loaded.register(lambda: start_daemons(daemons))
100 107
 
@@ -251,7 +258,20 @@ class CFG(object):
251 258
         )
252 259
         self.RADICALE_SERVER_ALLOW_ORIGIN = tg.config.get(
253 260
             'radicale.server.allow_origin',
254
-            '*',
261
+            None,
262
+        )
263
+        if not self.RADICALE_SERVER_ALLOW_ORIGIN:
264
+            self.RADICALE_SERVER_ALLOW_ORIGIN = self.WEBSITE_BASE_URL
265
+            logger.warning(
266
+                self,
267
+                'NOTE: Generated radicale.server.allow_origin parameter with '
268
+                'followings parameters: website.base_url ({0})'
269
+                .format(self.WEBSITE_BASE_URL)
270
+            )
271
+
272
+        self.RADICALE_SERVER_REALM_MESSAGE = tg.config.get(
273
+            'radicale.server.realm_message',
274
+            'Tracim Calendar - Password Required',
255 275
         )
256 276
 
257 277
         self.RADICALE_CLIENT_BASE_URL_TEMPLATE = \
@@ -271,6 +291,11 @@ class CFG(object):
271 291
                 .format(self.RADICALE_CLIENT_BASE_URL_TEMPLATE)
272 292
             )
273 293
 
294
+        self.USER_AUTH_TOKEN_VALIDITY = int(tg.config.get(
295
+            'user.auth_token.validity',
296
+            '604800',
297
+        ))
298
+
274 299
     def get_tracker_js_content(self, js_tracker_file_path = None):
275 300
         js_tracker_file_path = tg.config.get('js_tracker_path', None)
276 301
         if js_tracker_file_path:

+ 3 - 0
tracim/tracim/controllers/admin/user.py 查看文件

@@ -328,6 +328,9 @@ class UserRestController(TIMRestController):
328 328
             # Setup a random password to send email at user
329 329
             password = str(uuid.uuid4())
330 330
             user.password = password
331
+
332
+        user.webdav_left_digest_response_hash = '%s:/:%s' % (email, password)
333
+
331 334
         api.save(user)
332 335
 
333 336
         # Now add the user to related groups

+ 5 - 2
tracim/tracim/controllers/admin/workspace.py 查看文件

@@ -10,6 +10,7 @@ from tracim.controllers import TIMRestPathContextSetup
10 10
 
11 11
 from tracim.lib import CST
12 12
 from tracim.lib.base import BaseController
13
+from tracim.lib.helpers import on_off_to_boolean
13 14
 from tracim.lib.user import UserApi
14 15
 from tracim.lib.userworkspace import RoleApi
15 16
 from tracim.lib.content import ContentApi
@@ -191,10 +192,11 @@ class WorkspaceRestController(TIMRestController, BaseController):
191 192
         return dict(result = dictified_workspace, fake_api = fake_api)
192 193
 
193 194
     @tg.expose()
194
-    def post(self, name, description, calendar_enabled=False):
195
+    def post(self, name, description, calendar_enabled: str='off'):
195 196
         # FIXME - Check user profile
196 197
         user = tmpl_context.current_user
197 198
         workspace_api_controller = WorkspaceApi(user)
199
+        calendar_enabled = on_off_to_boolean(calendar_enabled)
198 200
 
199 201
         workspace = workspace_api_controller.create_workspace(name, description)
200 202
         workspace.calendar_enabled = calendar_enabled
@@ -215,9 +217,10 @@ class WorkspaceRestController(TIMRestController, BaseController):
215 217
         return DictLikeClass(result = dictified_workspace)
216 218
 
217 219
     @tg.expose('tracim.templates.workspace.edit')
218
-    def put(self, id, name, description, calendar_enabled):
220
+    def put(self, id, name, description, calendar_enabled: str='off'):
219 221
         user = tmpl_context.current_user
220 222
         workspace_api_controller = WorkspaceApi(user)
223
+        calendar_enabled = on_off_to_boolean(calendar_enabled)
221 224
 
222 225
         workspace = workspace_api_controller.get_one(id)
223 226
         workspace.label = name

+ 58 - 0
tracim/tracim/controllers/calendar.py 查看文件

@@ -0,0 +1,58 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+import tg
4
+from tg import tmpl_context
5
+
6
+from tracim.lib.base import BaseController
7
+from tracim.lib.calendar import CalendarManager
8
+from tracim.model.serializers import Context
9
+from tracim.model.serializers import CTX
10
+from tracim.model.serializers import DictLikeClass
11
+
12
+
13
+class CalendarController(BaseController):
14
+    """
15
+    Calendar web tracim page.
16
+    """
17
+
18
+    @tg.expose('tracim.templates.calendar.iframe_container')
19
+    def index(self):
20
+        user = tmpl_context.identity.get('user')
21
+        dictified_current_user = Context(CTX.CURRENT_USER).toDict(user)
22
+
23
+        fake_api = DictLikeClass(
24
+            current_user=dictified_current_user,
25
+        )
26
+
27
+        return DictLikeClass(fake_api=fake_api)
28
+
29
+
30
+class CalendarConfigController(BaseController):
31
+    """
32
+    CalDavZap javascript config generation
33
+    """
34
+
35
+    @tg.expose('tracim.templates.calendar.config')
36
+    def index(self):
37
+        # TODO BS 20160720: S'assurer d'être identifié !
38
+        user = tmpl_context.identity.get('user')
39
+        dictified_current_user = Context(CTX.CURRENT_USER).toDict(user)
40
+
41
+        fake_api = DictLikeClass(
42
+            current_user=dictified_current_user,
43
+        )
44
+        user_base_url = CalendarManager.get_user_base_url()
45
+        workspace_base_url = CalendarManager.get_workspace_base_url()
46
+        workspace_calendar_urls = CalendarManager\
47
+            .get_workspace_readable_calendars_urls_for_user(user)
48
+
49
+        # Template will use User.auth_token, ensure it's validity
50
+        user.ensure_auth_token()
51
+
52
+        return DictLikeClass(
53
+            fake_api=fake_api,
54
+            user_base_url=user_base_url,
55
+            workspace_base_url=workspace_base_url,
56
+            workspace_clendar_urls=workspace_calendar_urls,
57
+            auth_token=user.auth_token,
58
+        )

+ 5 - 1
tracim/tracim/controllers/root.py 查看文件

@@ -22,6 +22,8 @@ from tracim.controllers.admin import AdminController
22 22
 from tracim.controllers.debug import DebugController
23 23
 from tracim.controllers.error import ErrorController
24 24
 from tracim.controllers.help import HelpController
25
+from tracim.controllers.calendar import CalendarController
26
+from tracim.controllers.calendar import CalendarConfigController
25 27
 from tracim.controllers.user import UserRestController
26 28
 from tracim.controllers.workspace import UserWorkspaceRestController
27 29
 from tracim.lib.utils import replace_reset_password_templates
@@ -48,6 +50,8 @@ class RootController(StandardController):
48 50
 
49 51
     admin = AdminController()
50 52
     help = HelpController()
53
+    calendar = CalendarController()
54
+    calendar_config = CalendarConfigController()
51 55
 
52 56
     debug = DebugController()
53 57
     error = ErrorController()
@@ -144,7 +148,7 @@ class RootController(StandardController):
144 148
         # - last activity
145 149
         # - oldest open stuff
146 150
 
147
-        items = ContentApi(user).get_all(None, ContentType.Any, None)[:4]
151
+        items = ContentApi(user).get_all_without_exception(ContentType.Any, None)[:4]
148 152
         fake_api.favorites = Context(CTX.CONTENT_LIST).toDict(items, 'contents', 'nb')
149 153
         return DictLikeClass(fake_api=fake_api)
150 154
 

+ 1 - 0
tracim/tracim/controllers/user.py 查看文件

@@ -123,6 +123,7 @@ class UserPasswordRestController(TIMRestController):
123 123
             tg.redirect(redirect_url)
124 124
 
125 125
         current_user.password = new_password1
126
+        current_user.webdav_left_digest_response_hash = '%s:/:%s' % (current_user.email, new_password1)
126 127
         pm.DBSession.flush()
127 128
 
128 129
         tg.flash(_('Your password has been changed'))

二進制
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.mo 查看文件


+ 1 - 1
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.po 查看文件

@@ -1694,7 +1694,7 @@ msgstr "Page créée le {date} à {time} par <b>{author}</b>"
1694 1694
 
1695 1695
 #: tracim/templates/page/getone.mak:60
1696 1696
 msgid "You are reading <b>an old revision</b> of the current page. (the shown revision is r{})."
1697
-msgstr "Vous consultez <b>une ancienne version</b> de al page courante. (la version affichée est la v{})."
1697
+msgstr "Vous consultez <b>une ancienne version</b> de la page courante. (la version affichée est la v{})."
1698 1698
 
1699 1699
 #: tracim/templates/page/getone.mak:72 tracim/templates/thread/getone.mak:61
1700 1700
 msgid "<b>This information is deprecated</b>"

+ 13 - 1
tracim/tracim/lib/auth/internal.py 查看文件

@@ -5,6 +5,8 @@ from tg.configuration.auth import TGAuthMetadata
5 5
 from tracim.lib.auth.base import Auth
6 6
 from tracim.model import DBSession, User
7 7
 
8
+# TODO : temporary fix to update DB, to remove
9
+import transaction
8 10
 
9 11
 class InternalAuth(Auth):
10 12
 
@@ -27,15 +29,25 @@ class InternalApplicationAuthMetadata(TGAuthMetadata):
27 29
     def __init__(self, sa_auth):
28 30
         self.sa_auth = sa_auth
29 31
 
30
-    def authenticate(self, environ, identity):
32
+    def authenticate(self, environ, identity, allow_auth_token: bool=False):
31 33
         user = self.sa_auth.dbsession.query(self.sa_auth.user_class).filter(and_(
32 34
             self.sa_auth.user_class.is_active == True,
33 35
             self.sa_auth.user_class.email == identity['login']
34 36
         )).first()
35 37
 
36 38
         if user and user.validate_password(identity['password']):
39
+            if user.webdav_left_digest_response_hash == '':
40
+                user.webdav_left_digest_response_hash = '%s:/:%s' % (identity['login'], identity['password'])
41
+                DBSession.flush()
42
+                # TODO : temporary fix to update DB, to remove
43
+                transaction.commit()
37 44
             return identity['login']
38 45
 
46
+        if user and allow_auth_token:
47
+            user.ensure_auth_token()
48
+            if user.auth_token == identity['password']:
49
+                return identity['login']
50
+
39 51
     def get_user(self, identity, userid):
40 52
         return self.sa_auth.dbsession.query(self.sa_auth.user_class).filter(
41 53
             and_(self.sa_auth.user_class.is_active == True, self.sa_auth.user_class.email == userid)).first()

+ 17 - 1
tracim/tracim/lib/auth/ldap.py 查看文件

@@ -4,6 +4,7 @@ from tg.configuration.auth import TGAuthMetadata
4 4
 from who_ldap import LDAPAttributesPlugin as BaseLDAPAttributesPlugin, make_connection
5 5
 from who_ldap import LDAPGroupsPlugin as BaseLDAPGroupsPlugin
6 6
 from who_ldap import LDAPSearchAuthenticatorPlugin as BaseLDAPSearchAuthenticatorPlugin
7
+from sqlalchemy import and_
7 8
 
8 9
 from tracim.lib.auth.base import Auth
9 10
 from tracim.lib.exception import ConfigurationError
@@ -89,11 +90,26 @@ class LDAPSearchAuthenticatorPlugin(BaseLDAPSearchAuthenticatorPlugin):
89 90
     def set_auth(self, auth):
90 91
         self._auth = auth
91 92
 
92
-    def authenticate(self, environ, identity):
93
+    def authenticate(self, environ, identity, allow_auth_token: bool=False):
93 94
         # Note: super().authenticate return None if already authenticated or not found
94 95
         email = super().authenticate(environ, identity)
96
+
95 97
         if email:
96 98
             self._sync_ldap_user(email, environ, identity)
99
+
100
+        if not email and allow_auth_token and self.user_exist(email):
101
+            # Proceed to internal token auth
102
+            user = self.sa_auth.dbsession.query(self.sa_auth.user_class).filter(
103
+                and_(
104
+                    self.sa_auth.user_class.is_active == True,
105
+                    self.sa_auth.user_class.email == identity['login']
106
+                )
107
+            ).first()
108
+            if user:
109
+                user.ensure_auth_token()
110
+                if user.auth_token == identity['password']:
111
+                    email = identity['login']
112
+
97 113
         return email
98 114
 
99 115
     def _sync_ldap_user(self, email, environ, identity):

+ 38 - 0
tracim/tracim/lib/calendar.py 查看文件

@@ -10,6 +10,7 @@ from tracim.lib.exceptions import UnknownCalendarType
10 10
 from tracim.lib.exceptions import NotFound
11 11
 from tracim.lib.user import UserApi
12 12
 from tracim.lib.workspace import UnsafeWorkspaceApi
13
+from tracim.lib.workspace import WorkspaceApi
13 14
 from tracim.model import User
14 15
 from tracim.model import DBSession
15 16
 from tracim.model import new_revision
@@ -29,6 +30,9 @@ CALENDAR_TYPE_WORKSPACE = WorkspaceCalendar
29 30
 CALENDAR_USER_URL_TEMPLATE = 'user/{id}.ics/'
30 31
 CALENDAR_WORKSPACE_URL_TEMPLATE = 'workspace/{id}.ics/'
31 32
 
33
+CALENDAR_USER_BASE_URL = '/user/'
34
+CALENDAR_WORKSPACE_BASE_URL = '/workspace/'
35
+
32 36
 
33 37
 class CalendarManager(object):
34 38
     @classmethod
@@ -38,6 +42,18 @@ class CalendarManager(object):
38 42
         return cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE
39 43
 
40 44
     @classmethod
45
+    def get_user_base_url(cls):
46
+        from tracim.config.app_cfg import CFG
47
+        cfg = CFG.get_instance()
48
+        return os.path.join(cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE, 'user/')
49
+
50
+    @classmethod
51
+    def get_workspace_base_url(cls):
52
+        from tracim.config.app_cfg import CFG
53
+        cfg = CFG.get_instance()
54
+        return os.path.join(cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE, 'workspace/')
55
+
56
+    @classmethod
41 57
     def get_user_calendar_url(cls, user_id: int):
42 58
         user_path = CALENDAR_USER_URL_TEMPLATE.format(id=str(user_id))
43 59
         return os.path.join(cls.get_base_url(), user_path)
@@ -259,3 +275,25 @@ class CalendarManager(object):
259 275
             'start': event.get('dtend').dt.strftime('%Y-%m-%d %H:%M:%S%z'),
260 276
             'end': event.get('dtstart').dt.strftime('%Y-%m-%d %H:%M:%S%z'),
261 277
         }
278
+
279
+    @classmethod
280
+    def get_workspace_readable_calendars_urls_for_user(cls, user: User)\
281
+            -> [str]:
282
+        calendar_urls = []
283
+        workspace_api = WorkspaceApi(user)
284
+        for workspace in workspace_api.get_all_for_user(user):
285
+            if workspace.calendar_enabled:
286
+                calendar_urls.append(cls.get_workspace_calendar_url(
287
+                    workspace_id=workspace.workspace_id,
288
+                ))
289
+
290
+        return calendar_urls
291
+
292
+    def is_discovery_path(self, path: str) -> bool:
293
+        """
294
+        If collection url in one of them, Caldav client is tring to discover
295
+        collections.
296
+        :param path: collection path
297
+        :return: True if given collection path is an discover path
298
+        """
299
+        return path in ('user', 'workspace')

+ 115 - 4
tracim/tracim/lib/content.py 查看文件

@@ -80,6 +80,7 @@ class ContentApi(object):
80 80
             current_user: User,
81 81
             show_archived=False,
82 82
             show_deleted=False,
83
+            show_temporary=False,
83 84
             all_content_in_treeview=True,
84 85
             force_show_all_types=False,
85 86
             disable_user_workspaces_filter=False,
@@ -88,6 +89,7 @@ class ContentApi(object):
88 89
         self._user_id = current_user.user_id if current_user else None
89 90
         self._show_archived = show_archived
90 91
         self._show_deleted = show_deleted
92
+        self._show_temporary = show_temporary
91 93
         self._show_all_type_of_contents_in_treeview = all_content_in_treeview
92 94
         self._force_show_all_types = force_show_all_types
93 95
         self._disable_user_workspaces_filter = disable_user_workspaces_filter
@@ -200,6 +202,9 @@ class ContentApi(object):
200 202
         if not self._show_archived:
201 203
             result = result.filter(Content.is_archived==False)
202 204
 
205
+        if not self._show_temporary:
206
+            result = result.filter(Content.is_temporary==False)
207
+
203 208
         return result
204 209
 
205 210
     def __revisions_real_base_query(self, workspace: Workspace=None):
@@ -230,6 +235,9 @@ class ContentApi(object):
230 235
         if not self._show_archived:
231 236
             result = result.filter(ContentRevisionRO.is_archived==False)
232 237
 
238
+        if not self._show_temporary:
239
+            result = result.filter(Content.is_temporary==False)
240
+
233 241
         return result
234 242
 
235 243
     def _hard_filtered_base_query(self, workspace: Workspace=None):
@@ -256,6 +264,12 @@ class ContentApi(object):
256 264
                 filter(Content.is_archived==False).\
257 265
                 filter(parent.is_archived==False)
258 266
 
267
+        if not self._show_temporary:
268
+            parent = aliased(Content)
269
+            result = result.join(parent, Content.parent). \
270
+                filter(Content.is_temporary == False). \
271
+                filter(parent.is_temporary == False)
272
+
259 273
         return result
260 274
 
261 275
     def get_child_folders(self, parent: Content=None, workspace: Workspace=None, filter_by_allowed_content_types: list=[], removed_item_ids: list=[], allowed_node_types=None) -> [Content]:
@@ -302,7 +316,7 @@ class ContentApi(object):
302 316
 
303 317
         return result
304 318
 
305
-    def create(self, content_type: str, workspace: Workspace, parent: Content=None, label:str ='', do_save=False) -> Content:
319
+    def create(self, content_type: str, workspace: Workspace, parent: Content=None, label:str ='', is_temporary:bool =False, do_save=False) -> Content:
306 320
         assert content_type in ContentType.allowed_types()
307 321
         content = Content()
308 322
         content.owner = self._user
@@ -310,6 +324,7 @@ class ContentApi(object):
310 324
         content.workspace = workspace
311 325
         content.type = content_type
312 326
         content.label = label
327
+        content.is_temporary = is_temporary
313 328
         content.revision_type = ActionDescription.CREATION
314 329
 
315 330
         if do_save:
@@ -364,7 +379,75 @@ class ContentApi(object):
364 379
 
365 380
         return self._base_query(workspace).filter(Content.content_id==content_id).filter(Content.type==content_type).one()
366 381
 
367
-    def get_all(self, parent_id: int, content_type: str, workspace: Workspace=None) -> Content:
382
+    def get_one_revision(self, revision_id: int = None) -> ContentRevisionRO:
383
+        """
384
+        This method allow us to get directly any revision with its id
385
+        :param revision_id: The content's revision's id that we want to return
386
+        :return: An item Content linked with the correct revision
387
+        """
388
+        assert revision_id is not None# DYN_REMOVE
389
+
390
+        revision = DBSession.query(ContentRevisionRO).filter(ContentRevisionRO.revision_id == revision_id).one()
391
+
392
+        return revision
393
+
394
+    def get_one_by_label_and_parent(self, content_label: str, content_parent: Content = None,
395
+                                    workspace: Workspace = None) -> Content:
396
+        """
397
+        This method let us request the database to obtain a Content with its name and parent
398
+        :param content_label: Either the content's label or the content's filename if the label is None
399
+        :param content_parent: The parent's content
400
+        :param workspace: The workspace's content
401
+        :return The corresponding Content
402
+        """
403
+        assert content_label is not None# DYN_REMOVE
404
+
405
+        resultset = self._base_query(workspace)
406
+
407
+        parent_id = content_parent.content_id if content_parent else None
408
+
409
+        resultset = resultset.filter(Content.parent_id == parent_id)
410
+
411
+        return resultset.filter(or_(
412
+            Content.label == content_label,
413
+            Content.file_name == content_label,
414
+            Content.label == re.sub(r'\.[^.]+$', '', content_label)
415
+        )).one()
416
+
417
+    def get_one_by_label_and_parent_label(self, content_label: str, content_parent_label: [str]=None, workspace: Workspace=None):
418
+        assert content_label is not None  # DYN_REMOVE
419
+        resultset = self._base_query(workspace)
420
+
421
+        res =  resultset.filter(or_(
422
+            Content.label == content_label,
423
+            Content.file_name == content_label,
424
+            Content.label == re.sub(r'\.[^.]+$', '', content_label)
425
+        )).all()
426
+
427
+        if content_parent_label:
428
+            tmp = dict()
429
+            for content in res:
430
+                tmp[content] = content.parent
431
+
432
+            for parent_label in reversed(content_parent_label):
433
+                a = []
434
+                tmp = {content: parent.parent for content, parent in tmp.items()
435
+                       if parent and parent.label == parent_label}
436
+
437
+                if len(tmp) == 1:
438
+                    content, last_parent = tmp.popitem()
439
+                    return content
440
+                elif len(tmp) == 0:
441
+                    return None
442
+
443
+            for content, parent_content in tmp.items():
444
+                if not parent_content:
445
+                    return content
446
+
447
+            return None
448
+        return res[0]
449
+
450
+    def get_all(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> [Content]:
368 451
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
369 452
         assert content_type is not None# DYN_REMOVE
370 453
         assert isinstance(content_type, str) # DYN_REMOVE
@@ -374,8 +457,36 @@ class ContentApi(object):
374 457
         if content_type!=ContentType.Any:
375 458
             resultset = resultset.filter(Content.type==content_type)
376 459
 
377
-        if parent_id:
378
-            resultset = resultset.filter(Content.parent_id==parent_id)
460
+        resultset = resultset.filter(Content.parent_id==parent_id)
461
+
462
+        return resultset.all()
463
+
464
+    # TODO find an other name to filter on is_deleted / is_archived
465
+    def get_all_with_filter(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> [Content]:
466
+        assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
467
+        assert content_type is not None# DYN_REMOVE
468
+        assert isinstance(content_type, str) # DYN_REMOVE
469
+
470
+        resultset = self._base_query(workspace)
471
+
472
+        if content_type != ContentType.Any:
473
+            resultset = resultset.filter(Content.type==content_type)
474
+
475
+        resultset = resultset.filter(Content.is_deleted == self._show_deleted)
476
+        resultset = resultset.filter(Content.is_archived == self._show_archived)
477
+        resultset = resultset.filter(Content.is_temporary == self._show_temporary)
478
+
479
+        resultset = resultset.filter(Content.parent_id==parent_id)
480
+
481
+        return resultset.all()
482
+
483
+    def get_all_without_exception(self, content_type: str, workspace: Workspace=None) -> [Content]:
484
+        assert content_type is not None# DYN_REMOVE
485
+
486
+        resultset = self._base_query(workspace)
487
+
488
+        if content_type != ContentType.Any:
489
+            resultset = resultset.filter(Content.type==content_type)
379 490
 
380 491
         return resultset.all()
381 492
 

+ 164 - 5
tracim/tracim/lib/daemons.py 查看文件

@@ -179,6 +179,7 @@ class RadicaleDaemon(Daemon):
179 179
         tracim_storage = 'tracim.lib.radicale.storage'
180 180
         fs_path = cfg.RADICALE_SERVER_FILE_SYSTEM_FOLDER
181 181
         allow_origin = cfg.RADICALE_SERVER_ALLOW_ORIGIN
182
+        realm_message = cfg.RADICALE_SERVER_REALM_MESSAGE
182 183
 
183 184
         radicale_config.set('auth', 'type', 'custom')
184 185
         radicale_config.set('auth', 'custom_handler', tracim_auth)
@@ -190,18 +191,36 @@ class RadicaleDaemon(Daemon):
190 191
         radicale_config.set('storage', 'custom_handler', tracim_storage)
191 192
         radicale_config.set('storage', 'filesystem_folder', fs_path)
192 193
 
193
-        if allow_origin:
194
-            try:
195
-                radicale_config.add_section('headers')
196
-            except DuplicateSectionError:
197
-                pass  # It is not a problem, we just want it exist
194
+        radicale_config.set('server', 'realm', realm_message)
198 195
 
196
+        try:
197
+            radicale_config.add_section('headers')
198
+        except DuplicateSectionError:
199
+            pass  # It is not a problem, we just want it exist
200
+
201
+        if allow_origin:
199 202
             radicale_config.set(
200 203
                 'headers',
201 204
                 'Access-Control-Allow-Origin',
202 205
                 allow_origin,
203 206
             )
204 207
 
208
+        # Radicale is not 100% CALDAV Compliant, we force some Allow-Methods
209
+        radicale_config.set(
210
+            'headers',
211
+            'Access-Control-Allow-Methods',
212
+            'DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, OPTIONS, PROPFIND, '
213
+            'PROPPATCH, PUT, REPORT',
214
+        )
215
+
216
+        # Radicale is not 100% CALDAV Compliant, we force some Allow-Headers
217
+        radicale_config.set(
218
+            'headers',
219
+            'Access-Control-Allow-Headers',
220
+            'X-Requested-With,X-Auth-Token,Content-Type,Content-Length,'
221
+            'X-Client,Authorization,depth,Prefer,If-None-Match,If-Match',
222
+        )
223
+
205 224
     def _get_server(self):
206 225
         from tracim.config.app_cfg import CFG
207 226
         cfg = CFG.get_instance()
@@ -220,3 +239,143 @@ class RadicaleDaemon(Daemon):
220 239
         :param callback: callback to execute in daemon
221 240
         """
222 241
         self._server.append_thread_callback(callback)
242
+
243
+
244
+# TODO : webdav deamon, make it clean !
245
+
246
+import sys, os
247
+from wsgidav.wsgidav_app import DEFAULT_CONFIG
248
+from wsgidav.xml_tools import useLxml
249
+from wsgidav.wsgidav_app import WsgiDAVApp
250
+from wsgidav._version import __version__
251
+
252
+from tracim.lib.webdav.sql_dav_provider import Provider
253
+from tracim.lib.webdav.sql_domain_controller import TracimDomainController
254
+
255
+from inspect import isfunction
256
+import traceback
257
+
258
+DEFAULT_CONFIG_FILE = "wsgidav.conf"
259
+PYTHON_VERSION = "%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2])
260
+
261
+
262
+class WsgiDavDaemon(Daemon):
263
+
264
+    def __init__(self, *args, **kwargs):
265
+        super().__init__(*args, **kwargs)
266
+        self.config = self._initConfig()
267
+        self._server = None
268
+
269
+    def _initConfig(self):
270
+        """Setup configuration dictionary from default, command line and configuration file."""
271
+
272
+        # Set config defaults
273
+        config = DEFAULT_CONFIG.copy()
274
+        temp_verbose = config["verbose"]
275
+
276
+        # Configuration file overrides defaults
277
+        config_file = os.path.abspath(DEFAULT_CONFIG_FILE)
278
+        fileConf = self._readConfigFile(config_file, temp_verbose)
279
+        config.update(fileConf)
280
+
281
+        if not useLxml and config["verbose"] >= 1:
282
+            print(
283
+                "WARNING: Could not import lxml: using xml instead (slower). Consider installing lxml from http://codespeak.net/lxml/.")
284
+        from wsgidav.dir_browser import WsgiDavDirBrowser
285
+        from wsgidav.debug_filter import WsgiDavDebugFilter
286
+        from tracim.lib.webdav.tracim_http_authenticator import TracimHTTPAuthenticator
287
+        from wsgidav.error_printer import ErrorPrinter
288
+
289
+        config['middleware_stack'] = [ WsgiDavDirBrowser, TracimHTTPAuthenticator, ErrorPrinter, WsgiDavDebugFilter ]
290
+
291
+        config['provider_mapping'] = {
292
+            config['root_path']: Provider(
293
+                show_archived=config['show_archived'],
294
+                show_deleted=config['show_deleted'],
295
+                show_history=config['show_history'],
296
+                manage_locks=config['manager_locks']
297
+            )
298
+        }
299
+
300
+        config['domaincontroller'] = TracimDomainController(presetdomain=None, presetserver=None)
301
+
302
+        return config
303
+
304
+    def _readConfigFile(self, config_file, verbose):
305
+        """Read configuration file options into a dictionary."""
306
+
307
+        if not os.path.exists(config_file):
308
+            raise RuntimeError("Couldn't open configuration file '%s'." % config_file)
309
+
310
+        try:
311
+            import imp
312
+            conf = {}
313
+            configmodule = imp.load_source("configuration_module", config_file)
314
+
315
+            for k, v in vars(configmodule).items():
316
+                if k.startswith("__"):
317
+                    continue
318
+                elif isfunction(v):
319
+                    continue
320
+                conf[k] = v
321
+        except Exception as e:
322
+            exceptioninfo = traceback.format_exception_only(sys.exc_type, sys.exc_value)  # @UndefinedVariable
323
+            exceptiontext = ""
324
+            for einfo in exceptioninfo:
325
+                exceptiontext += einfo + "\n"
326
+
327
+            print("Failed to read configuration file: " + config_file + "\nDue to " + exceptiontext, file=sys.stderr)
328
+            raise
329
+
330
+        return conf
331
+
332
+    def run(self):
333
+        app = WsgiDAVApp(self.config)
334
+
335
+        # Try running WsgiDAV inside the following external servers:
336
+        self._runCherryPy(app, self.config, "cherrypy-bundled")
337
+
338
+    def _runCherryPy(self, app, config, mode):
339
+        """Run WsgiDAV using cherrypy.wsgiserver, if CherryPy is installed."""
340
+        assert mode in ("cherrypy", "cherrypy-bundled")
341
+
342
+        try:
343
+            from wsgidav.server.cherrypy import wsgiserver
344
+
345
+            version = "WsgiDAV/%s %s Python/%s" % (
346
+                __version__,
347
+                wsgiserver.CherryPyWSGIServer.version,
348
+                PYTHON_VERSION)
349
+
350
+            wsgiserver.CherryPyWSGIServer.version = version
351
+
352
+            protocol = "http"
353
+
354
+            if config["verbose"] >= 1:
355
+                print("Running %s" % version)
356
+                print("Listening on %s://%s:%s ..." % (protocol, config["host"], config["port"]))
357
+            self._server = wsgiserver.CherryPyWSGIServer(
358
+                (config["host"], config["port"]),
359
+                app,
360
+                server_name=version,
361
+            )
362
+
363
+            self._server.start()
364
+        except ImportError as e:
365
+            if config["verbose"] >= 1:
366
+                print("Could not import wsgiserver.CherryPyWSGIServer.")
367
+            return False
368
+        return True
369
+
370
+    def stop(self):
371
+        self._server.stop()
372
+
373
+    def append_thread_callback(self, callback: collections.Callable) -> None:
374
+        """
375
+        Place here the logic who permit to execute a callback in your daemon.
376
+        To get an exemple of that, take a look at
377
+        socketserver.BaseServer#service_actions  and how we use it in
378
+        tracim.lib.daemons.TracimSocketServerMixin#service_actions .
379
+        :param callback: callback to execute in your thread.
380
+        """
381
+        raise NotImplementedError()

+ 1 - 1
tracim/tracim/lib/radicale/auth.py 查看文件

@@ -22,7 +22,7 @@ class Auth(object):
22 22
         email = config.get('sa_auth').authmetadata.authenticate({}, {
23 23
             'login': user,
24 24
             'password': password
25
-        })
25
+        }, allow_auth_token=True)
26 26
         if email:
27 27
             cls.current_user = UserApi(None).get_one_by_email(email)
28 28
 

+ 4 - 0
tracim/tracim/lib/radicale/rights.py 查看文件

@@ -15,6 +15,10 @@ def authorized(user, collection, permission):
15 15
         return False
16 16
     current_user = UserApi(None).get_one_by_email(user)
17 17
     manager = CalendarManager(current_user)
18
+
19
+    if manager.is_discovery_path(collection.path):
20
+        return True
21
+
18 22
     try:
19 23
         calendar = manager.find_calendar_with_path(collection.path)
20 24
     except NotFound:

+ 2 - 1
tracim/tracim/lib/userworkspace.py 查看文件

@@ -92,7 +92,8 @@ class RoleApi(object):
92 92
         role.workspace = workspace
93 93
         role.role = role_level
94 94
         if with_notif is not None:
95
-            role.do_notify = with_notif
95
+            from tracim.lib.helpers import on_off_to_boolean
96
+            role.do_notify = on_off_to_boolean(with_notif)
96 97
         if flush:
97 98
             DBSession.flush()
98 99
         return role

+ 49 - 0
tracim/tracim/lib/webdav/README.md 查看文件

@@ -0,0 +1,49 @@
1
+## What is webdav ?
2
+
3
+Webdav's a extension of the HTTP protocol that introduces new requests (MKCOL, PROPFIND...) to ease the
4
+management of files on a distant server. More information can be found [here](https://tools.ietf.org/html/rfc4918).
5
+
6
+This project is based on the project [WSGIDav](https://github.com/mar10/wsgidav), which is an implementation of webdav in python that
7
+provides all basic needs to install a webdav on your server. This library intends to adapt WSGIDav implementation
8
+with the database based file management of Tracim, allowing users to access and modify files through their
9
+Window's file system.
10
+
11
+## Behavior to know about Windows' client and Webdav
12
+
13
+There are behaviors you may observe while using the windows' client for webdav which differ from other clients.
14
+
15
+* Window's will send twice each request. The first one as Anonymous, which will get a 401 Not Authorized, and then
16
+the second authentified which will proceed normally. This is the correct behavior that you have to observe for every webdav's client.
17
+In fact when sending
18
+The thing is that you'll observe only one request for clients like Debian's default filesystem because they cache the first response
19
+they got when sending an Anonymous request and they know that they have to send an authentication, thus they don't waste time sending
20
+twice the request.
21
+
22
+* When uploading new documents, windows will call twice put if the files does not exist. The first one with a length of 0 so that the resources
23
+can be locked before sending the whole file.
24
+
25
+* To display names, Windows won't use the displayName property but the object's path. Thus even if /a/b/c can link to a file named 'readme.txt' with
26
+   other webdav's client, you'll need to have a path named '/a/b/readme.txt' with Windows' client.
27
+
28
+## Known issues
29
+
30
+As for now, there's still some flaws or unexpected behaviors in the webdav implementation when using Windows' client.
31
+Though we could - and may - use github's issues to report them, we'll first write them down here to not pollute the
32
+original reports on tracim's _normal use_.
33
+
34
+* When moving files or folders from webdav to webdav, windows will warn you that this action may harm your computer. Though it won't say anything
35
+when moving files from your computer to webdav or vice-versa.
36
+
37
+* When deleting folders, Windows will go through all sub-folders and files recursively and request to delete them itself before going back to the main
38
+target. In our case it means it'll delete all sub-contents in database when we only want the parent to be in _deleted state_.
39
+Plus as a folder is _never empty_ (always contains .deleted and .archived) it won't even delete the folder because it'll first check if the target
40
+isn't empty before sending the final delete request.
41
+
42
+* Lock/unlock system is currently broken, thus it's not possible to work with office as it'll lock both the document and its temporary files and
43
+will be unable to unlock afterward, making it impossible to update them and making windows make a lot of unwanted request because the first failed.
44
+**Imma gonna correct it asap**.
45
+
46
+## Improving performances
47
+
48
+All request aren't still optimal and may take longer than what we want or expect. Here will be some tracks that will ease
49
+code refactoring to improve performances.

+ 142 - 0
tracim/tracim/lib/webdav/__init__.py 查看文件

@@ -0,0 +1,142 @@
1
+# coding: utf8
2
+
3
+import transaction
4
+from wsgidav import util
5
+from wsgidav import compat
6
+
7
+from tracim.lib.content import ContentApi
8
+from tracim.model import new_revision
9
+from tracim.model.data import ActionDescription
10
+from tracim.model.data import ContentType
11
+from tracim.model.data import Content
12
+from tracim.model.data import Workspace
13
+
14
+
15
+class HistoryType(object):
16
+    Deleted = 'deleted'
17
+    Archived = 'archived'
18
+    Standard = 'standard'
19
+    All = 'all'
20
+
21
+
22
+class SpecialFolderExtension(object):
23
+    Deleted = '/.deleted'
24
+    Archived = '/.archived'
25
+    History = '/.history'
26
+
27
+
28
+class FakeFileStream(object):
29
+    """
30
+    Fake a FileStream that we're giving to wsgidav to receive data and create files / new revisions
31
+
32
+    There's two scenarios :
33
+    - when a new file is created, wsgidav will call the method createEmptyResource and except to get a _DAVResource
34
+    which should have both 'beginWrite' and 'endWrite' method implemented
35
+    - when a file which already exists is updated, he's going to call the 'beginWrite' function of the _DAVResource
36
+    to get a filestream and write content in it
37
+
38
+    In the first case scenario, the transfer takes two part : it first create the resource (createEmptyResource)
39
+    then add its content (beginWrite, write, close..). If we went without this class, we would create two revision
40
+    of the file upon creating a new file, which is not what we want.
41
+    """
42
+
43
+    def __init__(self, content_api: ContentApi, workspace: Workspace, path: str,
44
+                 file_name: str='', content: Content=None, parent: Content=None):
45
+        """
46
+
47
+        :param content_api:
48
+        :param workspace:
49
+        :param path:
50
+        :param file_name:
51
+        :param content:
52
+        :param parent:
53
+        """
54
+        self._file_stream = compat.BytesIO()
55
+
56
+        self._file_name = file_name if file_name != '' else self._content.file_name
57
+        self._content = content
58
+        self._api = content_api
59
+        self._workspace = workspace
60
+        self._parent = parent
61
+        self._path = path
62
+
63
+    def getRefUrl(self) -> str:
64
+        """
65
+        As wsgidav expect to receive a _DAVResource upon creating a new resource, this method's result is used
66
+        by Windows client to establish both file's path and file's name
67
+        """
68
+        return self._path
69
+
70
+    def beginWrite(self, contentType) -> 'FakeFileStream':
71
+        """
72
+        Called by wsgidav, it expect a filestream which possess both 'write' and 'close' operation to write
73
+        the file content.
74
+        """
75
+        return self
76
+
77
+    def endWrite(self, withErrors: bool):
78
+        """
79
+        Called by request_server when finished writing everything.
80
+        As we call operation to create new content or revision in the close operation, called before endWrite, there
81
+        is nothing to do here.
82
+        """
83
+        pass
84
+
85
+    def write(self, s: str):
86
+        """
87
+        Called by request_server when writing content to files, we put it inside a filestream
88
+        """
89
+        self._file_stream.write(s)
90
+
91
+    def close(self):
92
+        """
93
+        Called by request_server when the file content has been written. We either add a new content or create
94
+        a new revision
95
+        """
96
+
97
+        self._file_stream.seek(0)
98
+
99
+        if self._content is None:
100
+            self.create_file()
101
+        else:
102
+            self.update_file()
103
+
104
+        transaction.commit()
105
+
106
+    def create_file(self):
107
+        """
108
+        Called when this is a new file; will create a new Content initialized with the correct content
109
+        """
110
+
111
+        is_temporary = self._file_name.startswith('.~') or self._file_name.startswith('~')
112
+
113
+        file = self._api.create(
114
+            content_type=ContentType.File,
115
+            workspace=self._workspace,
116
+            parent=self._parent,
117
+            is_temporary=is_temporary
118
+        )
119
+
120
+        self._api.update_file_data(
121
+            file,
122
+            self._file_name,
123
+            util.guessMimeType(self._file_name),
124
+            self._file_stream.read()
125
+        )
126
+
127
+        self._api.save(file, ActionDescription.CREATION)
128
+
129
+    def update_file(self):
130
+        """
131
+        Called when we're updating an existing content; we create a new revision and update the file content
132
+        """
133
+
134
+        with new_revision(self._content):
135
+            self._api.update_file_data(
136
+                self._content,
137
+                self._file_name,
138
+                util.guessMimeType(self._content.file_name),
139
+                self._file_stream.read()
140
+            )
141
+
142
+            self._api.save(self._content, ActionDescription.EDITION)

+ 291 - 0
tracim/tracim/lib/webdav/design.py 查看文件

@@ -0,0 +1,291 @@
1
+#coding: utf8
2
+from datetime import datetime
3
+
4
+from tracim.model.data import VirtualEvent
5
+from tracim.model.data import ContentType
6
+from tracim.model import data
7
+
8
+def create_readable_date(created, delta_from_datetime: datetime = None):
9
+    if not delta_from_datetime:
10
+        delta_from_datetime = datetime.now()
11
+
12
+    delta = delta_from_datetime - created
13
+
14
+    if delta.days > 0:
15
+        if delta.days >= 365:
16
+            aff = '%d year%s ago' % (delta.days / 365, 's' if delta.days / 365 >= 2 else '')
17
+        elif delta.days >= 30:
18
+            aff = '%d month%s ago' % (delta.days / 30, 's' if delta.days / 30 >= 2 else '')
19
+        else:
20
+            aff = '%d day%s ago' % (delta.days, 's' if delta.days >= 2 else '')
21
+    else:
22
+        if delta.seconds < 60:
23
+            aff = '%d second%s ago' % (delta.seconds, 's' if delta.seconds > 1 else '')
24
+        elif delta.seconds / 60 < 60:
25
+            aff = '%d minute%s ago' % (delta.seconds / 60, 's' if delta.seconds / 60 >= 2 else '')
26
+        else:
27
+            aff = '%d hour%s ago' % (delta.seconds / 3600, 's' if delta.seconds / 3600 >= 2 else '')
28
+
29
+    return aff
30
+
31
+def designPage(content: data.Content, content_revision: data.ContentRevisionRO) -> str:
32
+    f = open('tracim/lib/webdav/style.css', 'r')
33
+    style = f.read()
34
+    f.close()
35
+
36
+    hist = content.get_history()
37
+    histHTML = '<table class="table table-striped table-hover">'
38
+    for event in hist:
39
+        if isinstance(event, VirtualEvent):
40
+            date = event.create_readable_date()
41
+            _LABELS = {
42
+                'archiving': 'Item archived',
43
+                'content-comment': 'Item commented',
44
+                'creation': 'Item created',
45
+                'deletion': 'Item deleted',
46
+                'edition': 'item modified',
47
+                'revision': 'New revision',
48
+                'status-update': 'New status',
49
+                'unarchiving': 'Item unarchived',
50
+                'undeletion': 'Item undeleted',
51
+                'move': 'Item moved'
52
+            }
53
+
54
+            label = _LABELS[event.type.id]
55
+
56
+            histHTML += '''
57
+                <tr class="%s">
58
+                    <td class="my-align"><span class="label label-default"><i class="fa %s"></i> %s</span></td>
59
+                    <td>%s</td>
60
+                    <td>%s</td>
61
+                    <td>%s</td>
62
+                </tr>
63
+                ''' % ('warning' if event.id == content_revision.revision_id else '',
64
+                       event.type.icon,
65
+                       label,
66
+                       date,
67
+                       event.owner.display_name,
68
+                       '<i class="fa fa-caret-left"></i> shown' if event.id == content_revision.revision_id else '''<span><a class="revision-link" href="/.history/%s/(%s - %s) %s.html">(View revision)</a></span>''' % (
69
+                       content.label, event.id, event.type.id, event.ref_object.label) if event.type.id in ['revision', 'creation', 'edition'] else '')
70
+
71
+    histHTML += '</table>'
72
+
73
+    file = '''
74
+<html>
75
+<head>
76
+	<title>%s</title>
77
+	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
78
+	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
79
+	<style>%s</style>
80
+	<script type="text/javascript" src="/home/arnaud/Documents/css/script.js"></script>
81
+	<script
82
+			  src="https://code.jquery.com/jquery-3.1.0.min.js"
83
+			  integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s="
84
+			  crossorigin="anonymous"></script>
85
+</head>
86
+<body>
87
+    <div id="left" class="col-lg-8 col-md-12 col-sm-12 col-xs-12">
88
+        <div class="title page">
89
+            <div class="title-text">
90
+                <i class="fa fa-file-text-o title-icon page"></i>
91
+                <h1>%s</h1>
92
+                <h6>page created on <b>%s</b> by <b>%s</b></h6>
93
+            </div>
94
+            <div class="pull-right">
95
+                <div class="btn-group btn-group-vertical">
96
+                    <a class="btn btn-default">
97
+                        <i class="fa fa-external-link"></i> View in tracim</a>
98
+                    </a>
99
+                </div>
100
+            </div>
101
+        </div>
102
+        <div class="content col-xs-12 col-sm-12 col-md-12 col-lg-12">
103
+            %s
104
+        </div>
105
+    </div>
106
+    <div id="right" class="col-lg-4 col-md-12 col-sm-12 col-xs-12">
107
+        <h4>History</h4>
108
+        %s
109
+    </div>
110
+    <script type="text/javascript">
111
+        window.onload = function() {
112
+            file_location = window.location.href
113
+            file_location = file_location.replace(/\/[^/]*$/, '')
114
+            file_location = file_location.replace(/\/.history\/[^/]*$/, '')
115
+
116
+            $('.revision-link').each(function() {
117
+                $(this).attr('href', file_location + $(this).attr('href'))
118
+            });
119
+        }
120
+    </script>
121
+</body>
122
+</html>
123
+        ''' % (content_revision.label,
124
+               style,
125
+               content_revision.label,
126
+               content.created.strftime("%B %d, %Y at %H:%m"),
127
+               content.owner.display_name,
128
+               content_revision.description,
129
+               histHTML)
130
+
131
+    return file
132
+
133
+def designThread(content: data.Content, content_revision: data.ContentRevisionRO, comments) -> str:
134
+        f = open('tracim/lib/webdav/style.css', 'r')
135
+        style = f.read()
136
+        f.close()
137
+
138
+        hist = content.get_history()
139
+
140
+        allT = []
141
+        allT += comments
142
+        allT += hist
143
+        allT.sort(key=lambda x: x.created, reverse=True)
144
+
145
+        disc = ''
146
+        participants = {}
147
+        for t in allT:
148
+            if t.type == ContentType.Comment:
149
+                disc += '''
150
+                    <div class="row comment comment-row">
151
+                        <i class="fa fa-comment-o comment-icon"></i>
152
+                            <div class="comment-content">
153
+                            <h5>
154
+                                <span class="comment-author"><b>%s</b> wrote :</span>
155
+                                <div class="pull-right text-right">%s</div>
156
+                            </h5>
157
+                            %s
158
+                        </div>
159
+                    </div>
160
+                    ''' % (t.owner.display_name, create_readable_date(t.created), t.description)
161
+
162
+                if t.owner.display_name not in participants:
163
+                    participants[t.owner.display_name] = [1, t.created]
164
+                else:
165
+                    participants[t.owner.display_name][0] += 1
166
+            else:
167
+                if isinstance(t, VirtualEvent) and t.type.id != 'comment':
168
+                    _LABELS = {
169
+                        'archiving': 'Item archived',
170
+                        'content-comment': 'Item commented',
171
+                        'creation': 'Item created',
172
+                        'deletion': 'Item deleted',
173
+                        'edition': 'item modified',
174
+                        'revision': 'New revision',
175
+                        'status-update': 'New status',
176
+                        'unarchiving': 'Item unarchived',
177
+                        'undeletion': 'Item undeleted',
178
+                        'move': 'Item moved',
179
+                        'comment' : 'hmmm'
180
+                    }
181
+
182
+                    label = _LABELS[t.type.id]
183
+
184
+                    disc += '''
185
+                    <div class="%s row comment comment-row to-hide">
186
+                        <i class="fa %s comment-icon"></i>
187
+                            <div class="comment-content">
188
+                            <h5>
189
+                                <span class="comment-author"><b>%s</b></span>
190
+                                <div class="pull-right text-right">%s</div>
191
+                            </h5>
192
+                            %s %s
193
+                        </div>
194
+                    </div>
195
+                    ''' % ('warning' if t.id == content_revision.revision_id else '',
196
+                           t.type.icon,
197
+                           t.owner.display_name,
198
+                           t.create_readable_date(),
199
+                           label,
200
+                            '<i class="fa fa-caret-left"></i> shown' if t.id == content_revision.revision_id else '''<span><a class="revision-link" href="/.history/%s/%s-%s">(View revision)</a></span>''' % (
201
+                               content.label,
202
+                               t.id,
203
+                               t.ref_object.label) if t.type.id in ['revision', 'creation', 'edition'] else '')
204
+
205
+        page = '''
206
+<html>
207
+<head>
208
+	<title>%s</title>
209
+	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
210
+	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
211
+	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
212
+	<style>%s</style>
213
+	<script type="text/javascript" src="/home/arnaud/Documents/css/script.js"></script>
214
+</head>
215
+<body>
216
+    <div id="left" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
217
+        <div class="title thread">
218
+            <div class="title-text">
219
+                <i class="fa fa-comments-o title-icon thread"></i>
220
+                <h1>%s</h1>
221
+                <h6>thread created on <b>%s</b> by <b>%s</b></h6>
222
+            </div>
223
+            <div class="pull-right">
224
+                <div class="btn-group btn-group-vertical">
225
+                    <a class="btn btn-default" onclick="hide_elements()">
226
+                        <i id="hideshow" class="fa fa-eye-slash"></i> <span id="hideshowtxt" >Hide history</span></a>
227
+                    </a>
228
+                    <a class="btn btn-default">
229
+                        <i class="fa fa-external-link"></i> View in tracim</a>
230
+                    </a>
231
+                </div>
232
+            </div>
233
+        </div>
234
+        <div class="content col-xs-12 col-sm-12 col-md-12 col-lg-12">
235
+            <div class="description">
236
+                <span class="description-text">%s</span>
237
+            </div>
238
+            %s
239
+        </div>
240
+    </div>
241
+    <script type="text/javascript">
242
+        window.onload = function() {
243
+            file_location = window.location.href
244
+            file_location = file_location.replace(/\/[^/]*$/, '')
245
+            file_location = file_location.replace(/\/.history\/[^/]*$/, '')
246
+
247
+            $('.revision-link').each(function() {
248
+                $(this).attr('href', file_location + $(this).attr('href'))
249
+            });
250
+        }
251
+
252
+        function hide_elements() {
253
+            elems = document.getElementsByClassName('to-hide');
254
+            if (elems.length > 0) {
255
+                for(var i = 0; i < elems.length; i++) {
256
+                    $(elems[i]).addClass('to-show')
257
+                    $(elems[i]).hide();
258
+                }
259
+                while (elems.length>0) {
260
+                    $(elems[0]).removeClass('comment-row');
261
+                    $(elems[0]).removeClass('to-hide');
262
+                }
263
+                $('#hideshow').addClass('fa-eye').removeClass('fa-eye-slash');
264
+                $('#hideshowtxt').html('Show history');
265
+            }
266
+            else {
267
+                elems = document.getElementsByClassName('to-show');
268
+                for(var i = 0; i<elems.length; i++) {
269
+                    $(elems[0]).addClass('comment-row');
270
+                    $(elems[i]).addClass('to-hide');
271
+                    $(elems[i]).show();
272
+                }
273
+                while (elems.length>0) {
274
+                    $(elems[0]).removeClass('to-show');
275
+                }
276
+                $('#hideshow').removeClass('fa-eye').addClass('fa-eye-slash');
277
+                $('#hideshowtxt').html('Hide history');
278
+            }
279
+        }
280
+    </script>
281
+</body>
282
+</html>
283
+        ''' % (content_revision.label,
284
+               style,
285
+               content_revision.label,
286
+               content.created.strftime("%B %d, %Y at %H:%m"),
287
+               content.owner.display_name,
288
+               content_revision.description,
289
+               disc)
290
+
291
+        return page

+ 275 - 0
tracim/tracim/lib/webdav/lock_storage.py 查看文件

@@ -0,0 +1,275 @@
1
+import time
2
+
3
+from tracim.lib.webdav.sql_model import Lock, Url2Token
4
+from wsgidav import util
5
+from wsgidav.lock_manager import normalizeLockRoot, lockString, generateLockToken, validateLock
6
+from wsgidav.rw_lock import ReadWriteLock
7
+
8
+_logger = util.getModuleLogger(__name__)
9
+
10
+
11
+def from_dict_to_base(lock):
12
+    return Lock(
13
+        token=lock["token"],
14
+        depth=lock["depth"],
15
+        root=lock["root"],
16
+        type=lock["type"],
17
+        scopre=lock["scope"],
18
+        owner=lock["owner"],
19
+        timeout=lock["timeout"],
20
+        principal=lock["principal"],
21
+        expire=lock["expire"]
22
+    )
23
+
24
+
25
+def from_base_to_dict(lock):
26
+    return {
27
+        'token': lock.token,
28
+        'depth': lock.depth,
29
+        'root': lock.root,
30
+        'type': lock.type,
31
+        'scope': lock.scope,
32
+        'owner': lock.owner,
33
+        'timeout': lock.timeout,
34
+        'principal': lock.principal,
35
+        'expire': lock.expire
36
+    }
37
+
38
+
39
+class LockStorage(object):
40
+    LOCK_TIME_OUT_DEFAULT = 604800  # 1 week, in seconds
41
+    LOCK_TIME_OUT_MAX = 4 * 604800  # 1 month, in seconds
42
+
43
+    def __init__(self):
44
+        self._session = None# todo Session()
45
+        self._lock = ReadWriteLock()
46
+
47
+    def __repr__(self):
48
+        return "C'est bien mon verrou..."
49
+
50
+    def __del__(self):
51
+        pass
52
+
53
+    def get_lock_db_from_token(self, token):
54
+        return self._session.query(Lock).filter(Lock.token == token).one_or_none()
55
+
56
+    def _flush(self):
57
+        """Overloaded by Shelve implementation."""
58
+        pass
59
+
60
+    def open(self):
61
+        """Called before first use.
62
+
63
+        May be implemented to initialize a storage.
64
+        """
65
+        pass
66
+
67
+    def close(self):
68
+        """Called on shutdown."""
69
+        pass
70
+
71
+    def cleanup(self):
72
+        """Purge expired locks (optional)."""
73
+        pass
74
+
75
+    def clear(self):
76
+        """Delete all entries."""
77
+        self._session.query(Lock).all().delete(synchronize_session=False)
78
+        self._session.commit()
79
+
80
+    def get(self, token):
81
+        """Return a lock dictionary for a token.
82
+
83
+        If the lock does not exist or is expired, None is returned.
84
+
85
+        token:
86
+            lock token
87
+        Returns:
88
+            Lock dictionary or <None>
89
+
90
+        Side effect: if lock is expired, it will be purged and None is returned.
91
+        """
92
+        self._lock.acquireRead()
93
+        try:
94
+            lock_base = self._session.query(Lock).filter(Lock.token == token).one_or_none()
95
+            if lock_base is None:
96
+                # Lock not found: purge dangling URL2TOKEN entries
97
+                _logger.debug("Lock purged dangling: %s" % token)
98
+                self.delete(token)
99
+                return None
100
+            expire = float(lock_base.expire)
101
+            if 0 <= expire < time.time():
102
+                _logger.debug("Lock timed-out(%s): %s" % (expire, lockString(from_base_to_dict(lock_base))))
103
+                self.delete(token)
104
+                return None
105
+            return from_base_to_dict(lock_base)
106
+        finally:
107
+            self._lock.release()
108
+
109
+    def create(self, path, lock):
110
+        """Create a direct lock for a resource path.
111
+
112
+        path:
113
+            Normalized path (utf8 encoded string, no trailing '/')
114
+        lock:
115
+            lock dictionary, without a token entry
116
+        Returns:
117
+            New unique lock token.: <lock
118
+
119
+        **Note:** the lock dictionary may be modified on return:
120
+
121
+        - lock['root'] is ignored and set to the normalized <path>
122
+        - lock['timeout'] may be normalized and shorter than requested
123
+        - lock['token'] is added
124
+        """
125
+        self._lock.acquireWrite()
126
+        try:
127
+            # We expect only a lock definition, not an existing lock
128
+            assert lock.get("token") is None
129
+            assert lock.get("expire") is None, "Use timeout instead of expire"
130
+            assert path and "/" in path
131
+
132
+            # Normalize root: /foo/bar
133
+            org_path = path
134
+            path = normalizeLockRoot(path)
135
+            lock["root"] = path
136
+
137
+            # Normalize timeout from ttl to expire-date
138
+            timeout = float(lock.get("timeout"))
139
+            if timeout is None:
140
+                timeout = LockStorage.LOCK_TIME_OUT_DEFAULT
141
+            elif timeout < 0 or timeout > LockStorage.LOCK_TIME_OUT_MAX:
142
+                timeout = LockStorage.LOCK_TIME_OUT_MAX
143
+
144
+            lock["timeout"] = timeout
145
+            lock["expire"] = time.time() + timeout
146
+
147
+            validateLock(lock)
148
+
149
+            token = generateLockToken()
150
+            lock["token"] = token
151
+
152
+            # Store lock
153
+            lock_db = from_dict_to_base(lock)
154
+
155
+            self._session.add(lock_db)
156
+
157
+            # Store locked path reference
158
+            url2token = Url2Token(
159
+                path=path,
160
+                token=token
161
+            )
162
+
163
+            self._session.add(url2token)
164
+            self._session.commit()
165
+
166
+            self._flush()
167
+            _logger.debug("LockStorageDict.set(%r): %s" % (org_path, lockString(lock)))
168
+            #            print("LockStorageDict.set(%r): %s" % (org_path, lockString(lock)))
169
+            return lock
170
+        finally:
171
+            self._lock.release()
172
+
173
+    def refresh(self, token, timeout):
174
+        """Modify an existing lock's timeout.
175
+
176
+        token:
177
+            Valid lock token.
178
+        timeout:
179
+            Suggested lifetime in seconds (-1 for infinite).
180
+            The real expiration time may be shorter than requested!
181
+        Returns:
182
+            Lock dictionary.
183
+            Raises ValueError, if token is invalid.
184
+        """
185
+        lock_db = self._session.query(Lock).filter(Lock.token == token).one_or_none()
186
+        assert lock_db is not None, "Lock must exist"
187
+        assert timeout == -1 or timeout > 0
188
+        if timeout < 0 or timeout > LockStorage.LOCK_TIME_OUT_MAX:
189
+            timeout = LockStorage.LOCK_TIME_OUT_MAX
190
+
191
+        self._lock.acquireWrite()
192
+        try:
193
+            # Note: shelve dictionary returns copies, so we must reassign values:
194
+            lock_db.timeout = timeout
195
+            lock_db.expire = time.time() + timeout
196
+            self._session.commit()
197
+            self._flush()
198
+        finally:
199
+            self._lock.release()
200
+        return from_base_to_dict(lock_db)
201
+
202
+    def delete(self, token):
203
+        """Delete lock.
204
+
205
+        Returns True on success. False, if token does not exist, or is expired.
206
+        """
207
+        self._lock.acquireWrite()
208
+        try:
209
+            lock_db = self._session.query(Lock).filter(Lock.token == token).one_or_none()
210
+            _logger.debug("delete %s" % lockString(from_base_to_dict(lock_db)))
211
+            if lock_db is None:
212
+                return False
213
+            # Remove url to lock mapping
214
+            url2token = self._session.query(Url2Token).filter(
215
+                Url2Token.path == lock_db.root,
216
+                Url2Token.token == token).one_or_none()
217
+            if url2token is not None:
218
+                self._session.delete(url2token)
219
+            # Remove the lock
220
+            self._session.delete(lock_db)
221
+            self._session.commit()
222
+
223
+            self._flush()
224
+        finally:
225
+            self._lock.release()
226
+        return True
227
+
228
+    def getLockList(self, path, includeRoot, includeChildren, tokenOnly):
229
+        """Return a list of direct locks for <path>.
230
+
231
+        Expired locks are *not* returned (but may be purged).
232
+
233
+        path:
234
+            Normalized path (utf8 encoded string, no trailing '/')
235
+        includeRoot:
236
+            False: don't add <path> lock (only makes sense, when includeChildren
237
+            is True).
238
+        includeChildren:
239
+            True: Also check all sub-paths for existing locks.
240
+        tokenOnly:
241
+            True: only a list of token is returned. This may be implemented
242
+            more efficiently by some providers.
243
+        Returns:
244
+            List of valid lock dictionaries (may be empty).
245
+        """
246
+        assert path and path.startswith("/")
247
+        assert includeRoot or includeChildren
248
+
249
+        def __appendLocks(toklist):
250
+            # Since we can do this quickly, we use self.get() even if
251
+            # tokenOnly is set, so expired locks are purged.
252
+            for token in toklist:
253
+                lock_db = self.get_lock_db_from_token(token)
254
+                if lock_db:
255
+                    if tokenOnly:
256
+                        lockList.append(lock_db.token)
257
+                    else:
258
+                        lockList.append(from_base_to_dict(lock_db))
259
+
260
+        path = normalizeLockRoot(path)
261
+        self._lock.acquireRead()
262
+        try:
263
+            tokList = self._session.query(Url2Token.token).filter(Url2Token.path == path).all()
264
+            lockList = []
265
+            if includeRoot:
266
+                __appendLocks(tokList)
267
+
268
+            if includeChildren:
269
+                for url, in self._session.query(Url2Token.path).group_by(Url2Token.path):
270
+                    if util.isChildUri(path, url):
271
+                        __appendLocks(self._session.query(Url2Token.token).filter(Url2Token.path == url))
272
+
273
+            return lockList
274
+        finally:
275
+            self._lock.release()

+ 313 - 0
tracim/tracim/lib/webdav/sql_dav_provider.py 查看文件

@@ -0,0 +1,313 @@
1
+# coding: utf8
2
+
3
+import re
4
+from os.path import basename, dirname, normpath
5
+
6
+from wsgidav.dav_provider import DAVProvider
7
+from wsgidav.lock_manager import LockManager
8
+
9
+from tracim.lib.webdav import HistoryType, SpecialFolderExtension
10
+from tracim.lib.webdav import sql_resources
11
+from tracim.lib.webdav.lock_storage import LockStorage
12
+
13
+from tracim.lib.content import ContentApi
14
+from tracim.lib.content import ContentRevisionRO
15
+from tracim.lib.user import UserApi
16
+from tracim.lib.workspace import WorkspaceApi
17
+from tracim.model.data import Content, Workspace
18
+from tracim.model.data import ContentType
19
+
20
+
21
+class Provider(DAVProvider):
22
+    """
23
+    This class' role is to provide to wsgidav _DAVResource. Wsgidav will then use them to execute action and send
24
+    informations to the client
25
+    """
26
+
27
+    def __init__(self, show_history=True, show_deleted=True, show_archived=True, manage_locks=True):
28
+        super(Provider, self).__init__()
29
+
30
+        if manage_locks:
31
+            self.lockManager = LockManager(LockStorage())
32
+
33
+        self._show_archive = show_archived
34
+        self._show_delete = show_deleted
35
+        self._show_history = show_history
36
+
37
+    def show_history(self):
38
+        return self._show_history
39
+
40
+    def show_delete(self):
41
+        return self._show_delete
42
+
43
+    def show_archive(self):
44
+        return self._show_archive
45
+
46
+    #########################################################
47
+    # Everything override from DAVProvider
48
+    def getResourceInst(self, path: str, environ: dict):
49
+        """
50
+        Called by wsgidav whenever a request is called to get the _DAVResource corresponding to the path
51
+        """
52
+        if not self.exists(path, environ):
53
+            return None
54
+
55
+        path = normpath(path)
56
+        root_path = environ['http_authenticator.realm']
57
+
58
+        # If the requested path is the root, then we return a Root resource
59
+        if path == root_path:
60
+            return sql_resources.Root(path, environ)
61
+
62
+        user = UserApi(None).get_one_by_email(environ['http_authenticator.username'])
63
+        workspace_api = WorkspaceApi(user)
64
+        workspace = self.get_workspace_from_path(path, workspace_api)
65
+
66
+        # If the request path is in the form root/name, then we return a Workspace resource
67
+        parent_path = dirname(path)
68
+        if parent_path == root_path:
69
+            if not workspace:
70
+                return None
71
+            return sql_resources.Workspace(path, environ, workspace)
72
+
73
+        # And now we'll work on the path to establish which type or resource is requested
74
+
75
+        content_api = ContentApi(
76
+            user,
77
+            show_archived=self._show_archive,
78
+            show_deleted=self._show_delete
79
+        )
80
+
81
+        content = self.get_content_from_path(
82
+            path=path,
83
+            content_api=content_api,
84
+            workspace=workspace
85
+        )
86
+
87
+
88
+        # Easy cases : path either end with /.deleted, /.archived or /.history, then we return corresponding resources
89
+        if path.endswith(SpecialFolderExtension.Archived) and self._show_archive:
90
+            return sql_resources.ArchivedFolder(path, environ, workspace, content)
91
+
92
+        if path.endswith(SpecialFolderExtension.Deleted) and self._show_delete:
93
+            return sql_resources.DeletedFolder(path, environ, workspace, content)
94
+
95
+        if path.endswith(SpecialFolderExtension.History) and self._show_history:
96
+            is_deleted_folder = re.search(r'/\.deleted/\.history$', path) is not None
97
+            is_archived_folder = re.search(r'/\.archived/\.history$', path) is not None
98
+
99
+            type = HistoryType.Deleted if is_deleted_folder \
100
+                else HistoryType.Archived if is_archived_folder \
101
+                else HistoryType.Standard
102
+
103
+            return sql_resources.HistoryFolder(path, environ, workspace, content, type)
104
+
105
+        # Now that's more complicated, we're trying to find out if the path end with /.history/file_name
106
+        is_history_file_folder = re.search(r'/\.history/([^/]+)$', path) is not None
107
+
108
+        if is_history_file_folder and self._show_history:
109
+            return sql_resources.HistoryFileFolder(
110
+                path=path,
111
+                environ=environ,
112
+                content=content
113
+            )
114
+
115
+        # And here next step :
116
+        is_history_file = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) .+', path) is not None
117
+
118
+        if self._show_history and is_history_file:
119
+
120
+            revision_id = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) ([^/].+)$', path).group(1)
121
+
122
+            content_revision = content_api.get_one_revision(revision_id)
123
+            content = self.get_content_from_revision(content_revision, content_api)
124
+
125
+            if content.type == ContentType.File:
126
+                return sql_resources.HistoryFile(path, environ, content, content_revision)
127
+            else:
128
+                return sql_resources.HistoryOtherFile(path, environ, content, content_revision)
129
+
130
+        # And if we're still going, the client is asking for a standard Folder/File/Page/Thread so we check the type7
131
+        # and return the corresponding resource
132
+
133
+        if content is None:
134
+            return None
135
+        if content.type == ContentType.Folder:
136
+            return sql_resources.Folder(path, environ, content.workspace, content)
137
+        elif content.type == ContentType.File:
138
+            return sql_resources.File(path, environ, content)
139
+        else:
140
+            return sql_resources.OtherFile(path, environ, content)
141
+
142
+    def exists(self, path, environ) -> bool:
143
+        """
144
+        Called by wsgidav to check if a certain path is linked to a _DAVResource
145
+        """
146
+
147
+        path = normpath(path)
148
+        working_path = self.reduce_path(path)
149
+        root_path = environ['http_authenticator.realm']
150
+        parent_path = dirname(working_path)
151
+
152
+        if path == root_path:
153
+            return True
154
+
155
+        user = UserApi(None).get_one_by_email(environ['http_authenticator.username'])
156
+
157
+        workspace = self.get_workspace_from_path(path, WorkspaceApi(user))
158
+
159
+        if parent_path == root_path or workspace is None:
160
+            return workspace is not None
161
+
162
+        content_api = ContentApi(user, show_archived=True, show_deleted=True)
163
+
164
+        revision_id = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) ([^/].+)$', path)
165
+
166
+        is_archived = self.is_path_archive(path)
167
+
168
+        is_deleted = self.is_path_delete(path)
169
+
170
+        if revision_id:
171
+            revision_id = revision_id.group(1)
172
+            content = content_api.get_one_revision(revision_id)
173
+        else:
174
+            content = self.get_content_from_path(working_path, content_api, workspace)
175
+
176
+        return content is not None \
177
+            and content.is_deleted == is_deleted \
178
+            and content.is_archived == is_archived
179
+
180
+    def is_path_archive(self, path):
181
+        """
182
+        This function will check if a given path is linked to a file that's archived or not. We're checking if the
183
+        given path end with one of these string :
184
+
185
+        ex:
186
+            - /a/b/.archived/my_file
187
+            - /a/b/.archived/.history/my_file
188
+            - /a/b/.archived/.history/my_file/(3615 - edition) my_file
189
+        """
190
+
191
+        return re.search(
192
+            r'/\.archived/(\.history/)?(?!\.history)[^/]*(/\.)?(history|deleted|archived)?$',
193
+            path
194
+        ) is not None
195
+
196
+    def is_path_delete(self, path):
197
+        """
198
+        This function will check if a given path is linked to a file that's deleted or not. We're checking if the
199
+        given path end with one of these string :
200
+
201
+        ex:
202
+            - /a/b/.deleted/my_file
203
+            - /a/b/.deleted/.history/my_file
204
+            - /a/b/.deleted/.history/my_file/(3615 - edition) my_file
205
+        """
206
+
207
+        return re.search(
208
+            r'/\.deleted/(\.history/)?(?!\.history)[^/]*(/\.)?(history|deleted|archived)?$',
209
+            path
210
+        ) is not None
211
+
212
+    def reduce_path(self, path: str) -> str:
213
+        """
214
+        As we use the given path to request the database
215
+
216
+        ex: if the path is /a/b/.deleted/c/.archived, we're trying to get the archived content of the 'c' resource,
217
+        we need to keep the path /a/b/c
218
+
219
+        ex: if the path is /a/b/.history/my_file, we're trying to get the history of the file my_file, thus we need
220
+        the path /a/b/my_file
221
+
222
+        ex: if the path is /a/b/.history/my_file/(1985 - edition) my_old_name, we're looking for,
223
+        thus we remove all useless information
224
+        """
225
+        path = re.sub(r'/\.archived', r'', path)
226
+        path = re.sub(r'/\.deleted', r'', path)
227
+        path = re.sub(r'/\.history/[^/]+/(\d+)-.+', r'/\1', path)
228
+        path = re.sub(r'/\.history/([^/]+)', r'/\1', path)
229
+        path = re.sub(r'/\.history', r'', path)
230
+
231
+        return path
232
+
233
+    def get_content_from_path(self, path, content_api: ContentApi, workspace: Workspace) -> Content:
234
+        """
235
+        Called whenever we want to get the Content item from the database for a given path
236
+        """
237
+        path = self.reduce_path(path)
238
+        parent_path = dirname(path)
239
+
240
+        blbl = parent_path.replace('/'+workspace.label, '')
241
+
242
+        parents = blbl.split('/')
243
+
244
+        parents.remove('')
245
+        parents = [self.transform_to_bdd(x) for x in parents]
246
+
247
+        try:
248
+            return content_api.get_one_by_label_and_parent_label(
249
+                self.transform_to_bdd(basename(path)),
250
+                parents,
251
+                workspace
252
+            )
253
+        except:
254
+            return None
255
+
256
+    def get_content_from_revision(self, revision: ContentRevisionRO, api: ContentApi) -> Content:
257
+        try:
258
+            return api.get_one(revision.content_id, ContentType.Any)
259
+        except:
260
+            return None
261
+
262
+    def get_parent_from_path(self, path, api: ContentApi, workspace) -> Content:
263
+        return self.get_content_from_path(dirname(path), api, workspace)
264
+
265
+    def get_workspace_from_path(self, path: str, api: WorkspaceApi) -> Workspace:
266
+        try:
267
+            return api.get_one_by_label(self.transform_to_bdd(path.split('/')[1]))
268
+        except:
269
+            return None
270
+
271
+    def transform_to_display(self, string):
272
+        """
273
+        As characters that Windows does not support may have been inserted through Tracim in names, before displaying
274
+        information we update path so that all these forbidden characters are replaced with similar shape character
275
+        that are allowed so that the user isn't trouble and isn't limited in his naming choice
276
+        """
277
+        _TO_DISPLAY = {
278
+            '/':'⧸',
279
+            '\\': '⧹',
280
+            ':': '∶',
281
+            '*': '∗',
282
+            '?': 'ʔ',
283
+            '"': 'ʺ',
284
+            '<': '❮',
285
+            '>': '❯',
286
+            '|': '∣'
287
+        }
288
+
289
+        for key, value in _TO_DISPLAY.items():
290
+            string = string.replace(key, value)
291
+
292
+        return string
293
+
294
+    def transform_to_bdd(self, string):
295
+        """
296
+        Called before sending request to the database to recover the right names
297
+        """
298
+        _TO_BDD = {
299
+            '⧸': '/',
300
+            '⧹': '\\',
301
+            '∶': ':',
302
+            '∗': '*',
303
+            'ʔ': '?',
304
+            'ʺ': '"',
305
+            '❮': '<',
306
+            '❯': '>',
307
+            '∣': '|'
308
+        }
309
+
310
+        for key, value in _TO_BDD.items():
311
+            string = string.replace(key, value)
312
+
313
+        return string

+ 48 - 0
tracim/tracim/lib/webdav/sql_domain_controller.py 查看文件

@@ -0,0 +1,48 @@
1
+# coding: utf8
2
+
3
+from tracim.lib.user import UserApi
4
+
5
+class TracimDomainController(object):
6
+    """
7
+    The domain controller is used by http_authenticator to authenticate the user every time a request is
8
+    sent
9
+    """
10
+    def __init__(self, presetdomain = None, presetserver = None):
11
+        self._api = UserApi(None)
12
+
13
+    def getDomainRealm(self, inputURL, environ):
14
+        return '/'
15
+
16
+    def requireAuthentication(self, realmname, environ):
17
+        return True
18
+
19
+    def isRealmUser(self, realmname, username, environ):
20
+        """
21
+        Called to check if for a given root, the username exists (though here we don't make difference between
22
+        root as we're always starting at tracim's root
23
+        """
24
+        try:
25
+            self._api.get_one_by_email(username)
26
+            return True
27
+        except:
28
+            return False
29
+
30
+    def get_left_digest_response_hash(self, realmname, username, environ):
31
+        """
32
+        Called by our http_authenticator to get the hashed md5 digest for the current user that is also sent by
33
+        the webdav client
34
+        """
35
+        try:
36
+            user = self._api.get_one_by_email(username)
37
+            return user.webdav_left_digest_response_hash
38
+        except:
39
+            return None
40
+
41
+    def authDomainUser(self, realmname, username, password, environ):
42
+        """
43
+        If you ever feel the need to send a request al-mano with a curl, this is the function that'll be called by
44
+        http_authenticator to validate the password sent
45
+        """
46
+
47
+        return self.isRealmUser(realmname, username, environ) and \
48
+            self._api.get_one_by_email(username).validate_password(password)

+ 27 - 0
tracim/tracim/lib/webdav/sql_model.py 查看文件

@@ -0,0 +1,27 @@
1
+#coding: utf8
2
+
3
+from sqlalchemy import Column
4
+from sqlalchemy import ForeignKey
5
+from sqlalchemy.types import Unicode, UnicodeText, Float
6
+
7
+from wsgidav.compat import to_unicode
8
+
9
+class Lock(object):
10
+    __tablename__ = 'my_locks'
11
+
12
+    token = Column(UnicodeText, primary_key=True, unique=True, nullable=False)
13
+    depth = Column(Unicode(32), unique=False, nullable=False, default=to_unicode('infinity'))
14
+    root = Column(UnicodeText, unique=False, nullable=False)
15
+    type = Column(Unicode(32), unique=False, nullable=False, default=to_unicode('write'))
16
+    scope = Column(Unicode(32), unique=False, nullable=False, default=to_unicode('exclusive'))
17
+    owner = Column(UnicodeText, unique=False, nullable=False)
18
+    expire = Column(Float, unique=False, nullable=False)
19
+    principal = Column(Unicode(255), ForeignKey('my_users.display_name', ondelete="CASCADE"))
20
+    timeout = Column(Float, unique=False, nullable=False)
21
+
22
+
23
+class Url2Token(object):
24
+    __tablename__ = 'my_url2token'
25
+
26
+    token = Column(UnicodeText, primary_key=True, unique=True, nullable=False)
27
+    path = Column(UnicodeText, primary_key=True, unique=False, nullable=False)

文件差異過大導致無法顯示
+ 1089 - 0
tracim/tracim/lib/webdav/sql_resources.py


+ 100 - 0
tracim/tracim/lib/webdav/style.css 查看文件

@@ -0,0 +1,100 @@
1
+.title {
2
+	background:#F5F5F5;
3
+	padding-right:15px;
4
+	padding-left:15px;
5
+	padding-top:10px;
6
+	border-bottom:1px solid #CCCCCC;
7
+	overflow:auto;
8
+} .title h1 { margin-top:0; }
9
+
10
+.content {
11
+	padding: 15px;
12
+}
13
+
14
+#left{ padding:0; }
15
+
16
+#right {
17
+	background:#F5F5F5;
18
+	border-left:1px solid #CCCCCC;
19
+	border-bottom: 1px solid #CCCCCC;
20
+	padding-top:15px;
21
+}
22
+@media (max-width: 1200px) {
23
+	#right {
24
+		border-top:1px solid #CCCCCC;
25
+		border-left: none;
26
+		border-bottom: none;
27
+	}
28
+}
29
+
30
+body { overflow:auto; }
31
+
32
+.btn {
33
+	text-align: left;
34
+}
35
+
36
+.table tbody tr .my-align {
37
+	vertical-align:middle;
38
+}
39
+
40
+.title-icon {
41
+	font-size:2.5em;
42
+	float:left;
43
+	margin-right:10px;
44
+}
45
+.title.page, .title-icon.page { color:#00CC00; }
46
+.title.thread, .title-icon.thread { color:#428BCA; }
47
+
48
+/* ****************************** */
49
+.description-icon {
50
+	color:#999;
51
+	font-size:3em;
52
+}
53
+
54
+.description {
55
+	border-left: 5px solid #999;
56
+	padding-left: 10px;
57
+	margin-left: 10px;
58
+	margin-bottom:10px;
59
+}
60
+
61
+.description-text {
62
+	display:block;
63
+	overflow:hidden;
64
+	color:#999;
65
+}
66
+
67
+.comment-row:nth-child(2n) {
68
+	background-color:#F5F5F5;
69
+}
70
+
71
+.comment-row:nth-child(2n+1) {
72
+	background-color:#FFF;
73
+}
74
+
75
+.comment-icon {
76
+	color:#CCC;
77
+	font-size:3em;
78
+	display:inline-block;
79
+	margin-right: 10px;
80
+	float:left;
81
+}
82
+
83
+.comment-content {
84
+	display:block;
85
+	overflow:hidden;
86
+}
87
+
88
+.comment, .comment-revision {
89
+	padding:10px;
90
+	border-top: 1px solid #999;
91
+}
92
+
93
+.comment-revision-icon {
94
+	color:#777;
95
+	margin-right: 10px;
96
+}
97
+
98
+.title-text {
99
+	display: inline-block;
100
+}

+ 145 - 0
tracim/tracim/lib/webdav/tracim_http_authenticator.py 查看文件

@@ -0,0 +1,145 @@
1
+from wsgidav.http_authenticator import HTTPAuthenticator
2
+from wsgidav import util
3
+import re
4
+
5
+_logger = util.getModuleLogger(__name__, True)
6
+HOTFIX_WINXP_AcceptRootShareLogin = True
7
+
8
+
9
+class TracimHTTPAuthenticator(HTTPAuthenticator):
10
+    def __init__(self, application, config):
11
+        super(TracimHTTPAuthenticator, self).__init__(application, config)
12
+        self._headerfixparser = re.compile(r'([\w]+)=("[^"]*,[^"]*"),')
13
+
14
+    def authDigestAuthRequest(self, environ, start_response):
15
+        realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"], environ)
16
+
17
+        isinvalidreq = False
18
+
19
+        authheaderdict = dict([])
20
+        authheaders = environ["HTTP_AUTHORIZATION"] + ","
21
+        if not authheaders.lower().strip().startswith("digest"):
22
+            isinvalidreq = True
23
+            # Hotfix for Windows file manager and OSX Finder:
24
+        # Some clients don't urlencode paths in auth header, so uri value may
25
+        # contain commas, which break the usual regex headerparser. Example:
26
+        # Digest username="user",realm="/",uri="a,b.txt",nc=00000001, ...
27
+        # -> [..., ('uri', '"a'), ('nc', '00000001'), ...]
28
+        # Override any such values with carefully extracted ones.
29
+        authheaderlist = self._headerparser.findall(authheaders)
30
+        authheaderfixlist = self._headerfixparser.findall(authheaders)
31
+        if authheaderfixlist:
32
+            _logger.info("Fixing authheader comma-parsing: extend %s with %s" \
33
+                         % (authheaderlist, authheaderfixlist))
34
+            authheaderlist += authheaderfixlist
35
+        for authheader in authheaderlist:
36
+            authheaderkey = authheader[0]
37
+            authheadervalue = authheader[1].strip().strip("\"")
38
+            authheaderdict[authheaderkey] = authheadervalue
39
+
40
+        _logger.debug("authDigestAuthRequest: %s" % environ["HTTP_AUTHORIZATION"])
41
+        _logger.debug("  -> %s" % authheaderdict)
42
+
43
+        if "username" in authheaderdict:
44
+            req_username = authheaderdict["username"]
45
+            req_username_org = req_username
46
+            # Hotfix for Windows XP:
47
+            #   net use W: http://127.0.0.1/dav /USER:DOMAIN\tester tester
48
+            # will send the name with double backslashes ('DOMAIN\\tester')
49
+            # but send the digest for the simple name ('DOMAIN\tester').
50
+            if r"\\" in req_username:
51
+                req_username = req_username.replace("\\\\", "\\")
52
+                _logger.info("Fixing Windows name with double backslash: '%s' --> '%s'" % (req_username_org, req_username))
53
+
54
+            if not self._domaincontroller.isRealmUser(realmname, req_username, environ):
55
+                isinvalidreq = True
56
+        else:
57
+            isinvalidreq = True
58
+
59
+            # TODO: Chun added this comments, but code was commented out
60
+            # Do not do realm checking - a hotfix for WinXP using some other realm's
61
+            # auth details for this realm - if user/password match
62
+
63
+        if 'realm' in authheaderdict:
64
+            if authheaderdict["realm"].upper() != realmname.upper():
65
+                if HOTFIX_WINXP_AcceptRootShareLogin:
66
+                    # Hotfix: also accept '/'
67
+                    if authheaderdict["realm"].upper() != "/":
68
+                        isinvalidreq = True
69
+                else:
70
+                    isinvalidreq = True
71
+
72
+        if "algorithm" in authheaderdict:
73
+            if authheaderdict["algorithm"].upper() != "MD5":
74
+                isinvalidreq = True  # only MD5 supported
75
+
76
+        if "uri" in authheaderdict:
77
+            req_uri = authheaderdict["uri"]
78
+
79
+        if "nonce" in authheaderdict:
80
+            req_nonce = authheaderdict["nonce"]
81
+        else:
82
+            isinvalidreq = True
83
+
84
+        req_hasqop = False
85
+        if "qop" in authheaderdict:
86
+            req_hasqop = True
87
+            req_qop = authheaderdict["qop"]
88
+            if req_qop.lower() != "auth":
89
+                isinvalidreq = True  # only auth supported, auth-int not supported
90
+        else:
91
+            req_qop = None
92
+
93
+        if "cnonce" in authheaderdict:
94
+            req_cnonce = authheaderdict["cnonce"]
95
+        else:
96
+            req_cnonce = None
97
+            if req_hasqop:
98
+                isinvalidreq = True
99
+
100
+        if "nc" in authheaderdict:  # is read but nonce-count checking not implemented
101
+            req_nc = authheaderdict["nc"]
102
+        else:
103
+            req_nc = None
104
+            if req_hasqop:
105
+                isinvalidreq = True
106
+
107
+        if "response" in authheaderdict:
108
+            req_response = authheaderdict["response"]
109
+        else:
110
+            isinvalidreq = True
111
+
112
+        if not isinvalidreq:
113
+            left_digest_response_hash = self._domaincontroller.get_left_digest_response_hash(realmname, req_username, environ)
114
+
115
+            req_method = environ["REQUEST_METHOD"]
116
+
117
+            required_digest = self.tracim_compute_digest_response(left_digest_response_hash, req_method, req_uri, req_nonce,
118
+                                                         req_cnonce, req_qop, req_nc)
119
+
120
+            if required_digest != req_response:
121
+                _logger.warning("computeDigestResponse('%s', '%s', ...): %s != %s" % (
122
+                realmname, req_username, required_digest, req_response))
123
+                isinvalidreq = True
124
+            else:
125
+                _logger.debug("digest succeeded for realm '%s', user '%s'" % (realmname, req_username))
126
+            pass
127
+
128
+        if isinvalidreq:
129
+            _logger.warning("Authentication failed for user '%s', realm '%s'" % (req_username, realmname))
130
+            return self.sendDigestAuthResponse(environ, start_response)
131
+
132
+        environ["http_authenticator.realm"] = realmname
133
+        environ["http_authenticator.username"] = req_username
134
+        return self._application(environ, start_response)
135
+
136
+    def tracim_compute_digest_response(self, left_digest_response_hash, method, uri, nonce, cnonce, qop, nc):
137
+        # A1 : username:realm:password
138
+        A2 = method + ":" + uri
139
+        if qop:
140
+            digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + self.md5h(A2))
141
+        else:
142
+            digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + self.md5h(A2))
143
+            # print(A1, A2)
144
+        # print(digestresp)
145
+        return digestresp

+ 3 - 0
tracim/tracim/lib/workspace.py 查看文件

@@ -61,6 +61,9 @@ class WorkspaceApi(object):
61 61
     def get_one(self, id):
62 62
         return self._base_query().filter(Workspace.workspace_id==id).one()
63 63
 
64
+    def get_one_by_label(self, label: str) -> Workspace:
65
+        return self._base_query().filter(Workspace.label == label).one()
66
+
64 67
     """
65 68
     def get_one_for_current_user(self, id):
66 69
         return self._base_query().filter(Workspace.workspace_id==id).\

+ 46 - 0
tracim/tracim/model/auth.py 查看文件

@@ -8,12 +8,16 @@ It's perfectly fine to re-use this definition in the tracim application,
8 8
 though.
9 9
 
10 10
 """
11
+import uuid
12
+
11 13
 import os
12 14
 from datetime import datetime
15
+import time
13 16
 from hashlib import sha256
14 17
 from slugify import slugify
15 18
 from sqlalchemy.ext.hybrid import hybrid_property
16 19
 from tg.i18n import lazy_ugettext as l_
20
+from hashlib import md5
17 21
 
18 22
 __all__ = ['User', 'Group', 'Permission']
19 23
 
@@ -121,6 +125,9 @@ class User(DeclarativeBase):
121 125
     created = Column(DateTime, default=datetime.now)
122 126
     is_active = Column(Boolean, default=True, nullable=False)
123 127
     imported_from = Column(Unicode(32), nullable=True)
128
+    _webdav_left_digest_response_hash = Column('webdav_left_digest_response_hash', Unicode(128))
129
+    auth_token = Column(Unicode(255))
130
+    auth_token_created = Column(DateTime)
124 131
 
125 132
     @hybrid_property
126 133
     def email_address(self):
@@ -197,6 +204,20 @@ class User(DeclarativeBase):
197 204
     password = synonym('_password', descriptor=property(_get_password,
198 205
                                                         _set_password))
199 206
 
207
+    @classmethod
208
+    def _hash_digest(cls, digest):
209
+        return md5(bytes(digest, 'utf-8')).hexdigest()
210
+
211
+    def _set_hash_digest(self, digest):
212
+        self._webdav_left_digest_response_hash = self._hash_digest(digest)
213
+
214
+    def _get_hash_digest(self):
215
+        return self._webdav_left_digest_response_hash
216
+
217
+    webdav_left_digest_response_hash = synonym('_webdav_left_digest_response_hash',
218
+                                               descriptor=property(_get_hash_digest,
219
+                                                                    _set_hash_digest))
220
+
200 221
     def validate_password(self, password):
201 222
         """
202 223
         Check the password against existing credentials.
@@ -237,6 +258,31 @@ class User(DeclarativeBase):
237 258
         from tracim.model.data import UserRoleInWorkspace
238 259
         return UserRoleInWorkspace.NOT_APPLICABLE
239 260
 
261
+    def ensure_auth_token(self) -> None:
262
+        """
263
+        Create auth_token if None, regenerate auth_token if too much old.
264
+        auth_token validity is set in
265
+        :return:
266
+        """
267
+        from tracim.config.app_cfg import CFG
268
+        validity_seconds = CFG.get_instance().USER_AUTH_TOKEN_VALIDITY
269
+
270
+        if not self.auth_token or not self.auth_token_created:
271
+            self.auth_token = uuid.uuid4()
272
+            self.auth_token_created = datetime.utcnow()
273
+            DBSession.flush()
274
+            return
275
+
276
+        now_seconds = time.mktime(datetime.utcnow().timetuple())
277
+        auth_token_seconds = time.mktime(self.auth_token_created.timetuple())
278
+        difference = now_seconds - auth_token_seconds
279
+
280
+        if difference > validity_seconds:
281
+            self.auth_token = uuid.uuid4()
282
+            self.auth_token_created = datetime.utcnow()
283
+            DBSession.flush()
284
+
285
+
240 286
 class Permission(DeclarativeBase):
241 287
     """
242 288
     Permission definition.

+ 42 - 27
tracim/tracim/model/data.py 查看文件

@@ -533,6 +533,7 @@ class ContentRevisionRO(DeclarativeBase):
533 533
     updated = Column(DateTime, unique=False, nullable=False, default=datetime.utcnow)
534 534
     is_deleted = Column(Boolean, unique=False, nullable=False, default=False)
535 535
     is_archived = Column(Boolean, unique=False, nullable=False, default=False)
536
+    is_temporary = Column(Boolean, unique=False, nullable=False, default=False)
536 537
     revision_type = Column(Unicode(32), unique=False, nullable=False, default='')
537 538
 
538 539
     workspace_id = Column(Integer, ForeignKey('workspaces.workspace_id'), unique=False, nullable=True)
@@ -852,6 +853,18 @@ class Content(DeclarativeBase):
852 853
         return ContentRevisionRO.is_archived
853 854
 
854 855
     @hybrid_property
856
+    def is_temporary(self) -> bool:
857
+        return self.revision.is_temporary
858
+
859
+    @is_temporary.setter
860
+    def is_temporary(self, value: bool) -> None:
861
+        self.revision.is_temporary = value
862
+
863
+    @is_temporary.expression
864
+    def is_temporary(cls) -> InstrumentedAttribute:
865
+        return ContentRevisionRO.is_temporary
866
+
867
+    @hybrid_property
855 868
     def revision_type(self) -> str:
856 869
         return self.revision.revision_type
857 870
 
@@ -1008,30 +1021,6 @@ class Content(DeclarativeBase):
1008 1021
         return format_timedelta(delta_from_datetime - datetime_object,
1009 1022
                                 locale=tg.i18n.get_lang()[0])
1010 1023
 
1011
-
1012
-    def extract_links_from_content(self, other_content: str=None) -> [LinkItem]:
1013
-        """
1014
-        parse html content and extract links. By default, it works on the description property
1015
-        :param other_content: if not empty, then parse the given html content instead of description
1016
-        :return: a list of LinkItem
1017
-        """
1018
-        links = []
1019
-        return links
1020
-        soup = BeautifulSoup(
1021
-            self.description if not other_content else other_content,
1022
-            'html.parser'  # Fixes hanging bug - http://stackoverflow.com/questions/12618567/problems-running-beautifulsoup4-within-apache-mod-python-django
1023
-        )
1024
-
1025
-        for link in soup.findAll('a'):
1026
-            href = link.get('href')
1027
-            label = link.contents
1028
-            links.append(LinkItem(href, label))
1029
-        links.sort(key=lambda link: link.href if link.href else '')
1030
-
1031
-        sorted_links = sorted(links, key=lambda link: link.label if link.label else link.href, reverse=True)
1032
-        ## FIXME - Does this return a sorted list ???!
1033
-        return sorted_links
1034
-
1035 1024
     def get_child_nb(self, content_type: ContentType, content_status = ''):
1036 1025
         child_nb = 0
1037 1026
         for child in self.get_valid_children():
@@ -1203,7 +1192,8 @@ class VirtualEvent(object):
1203 1192
 
1204 1193
         label = content.get_label()
1205 1194
         if content.type==ContentType.Comment:
1206
-            label = _('<strong>{}</strong> wrote:').format(content.owner.get_display_name())
1195
+            # todo :voir le _('.... si le _ est utile
1196
+            label = ('<strong>{}</strong> wrote:').format(content.owner.get_display_name())
1207 1197
 
1208 1198
         return VirtualEvent(id=content.content_id,
1209 1199
                             created=content.created,
@@ -1234,7 +1224,7 @@ class VirtualEvent(object):
1234 1224
         self.content = content
1235 1225
         self.ref_object = ref_object
1236 1226
 
1237
-        print(type)
1227
+        # todo moi ? print(type)
1238 1228
         assert hasattr(type, 'id')
1239 1229
         assert hasattr(type, 'css')
1240 1230
         assert hasattr(type, 'icon')
@@ -1244,4 +1234,29 @@ class VirtualEvent(object):
1244 1234
         if not delta_from_datetime:
1245 1235
             delta_from_datetime = datetime.now()
1246 1236
         return format_timedelta(delta_from_datetime - self.created,
1247
-                                locale=tg.i18n.get_lang()[0])
1237
+                                locale=tg.i18n.get_lang()[0])
1238
+
1239
+    def create_readable_date(self, delta_from_datetime:datetime=None):
1240
+        aff = ''
1241
+
1242
+        if not delta_from_datetime:
1243
+            delta_from_datetime = datetime.now()
1244
+
1245
+        delta = delta_from_datetime - self.created
1246
+        
1247
+        if delta.days > 0:
1248
+            if delta.days >= 365:
1249
+                aff = '%d year%s ago' % (delta.days/365, 's' if delta.days/365>=2 else '')
1250
+            elif delta.days >= 30:
1251
+                aff = '%d month%s ago' % (delta.days/30, 's' if delta.days/30>=2 else '')
1252
+            else:
1253
+                aff = '%d day%s ago' % (delta.days, 's' if delta.days>=2 else '')
1254
+        else:
1255
+            if delta.seconds < 60:
1256
+                aff = '%d second%s ago' % (delta.seconds, 's' if delta.seconds>1 else '')
1257
+            elif delta.seconds/60 < 60:
1258
+                aff = '%d minute%s ago' % (delta.seconds/60, 's' if delta.seconds/60>=2 else '')
1259
+            else:
1260
+                aff = '%d hour%s ago' % (delta.seconds/3600, 's' if delta.seconds/3600>=2 else '')
1261
+
1262
+        return aff

+ 2 - 2
tracim/tracim/model/serializers.py 查看文件

@@ -382,7 +382,7 @@ def serialize_node_for_page(content: Content, context: Context):
382 382
             icon=ContentType.get_icon(content.type),
383 383
             owner=context.toDict(data_container.owner),
384 384
             status=context.toDict(data_container.get_status()),
385
-            links=context.toDict(content.extract_links_from_content(data_container.description)),
385
+            links=[],
386 386
             revisions=context.toDict(sorted(content.revisions, key=lambda v: v.created, reverse=True)),
387 387
             selected_revision='latest' if content.revision_to_serialize<=0 else content.revision_to_serialize,
388 388
             history=Context(CTX.CONTENT_HISTORY).toDict(content.get_history()),
@@ -445,7 +445,7 @@ def serialize_node_for_page(item: Content, context: Context):
445 445
             icon = ContentType.get_icon(item.type),
446 446
             id = item.content_id,
447 447
             label = item.label,
448
-            links = context.toDict(item.extract_links_from_content(item.description)),
448
+            links=[],
449 449
             owner = context.toDict(item.owner),
450 450
             parent = context.toDict(item.parent),
451 451
             selected_revision = 'latest',

+ 10 - 0
tracim/tracim/public/assets/css/dashboard.css 查看文件

@@ -372,3 +372,13 @@ span.info.readonly {
372 372
     font-size: 0.8em;
373 373
     opacity: 0.7;
374 374
 }
375
+
376
+@media(min-width: 768px) and (max-width: 992px) {
377
+    .download-file-button i.fa {
378
+        font-size: 12px;
379
+    }
380
+}
381
+
382
+.no-padding {
383
+    padding: 0px;
384
+}

+ 35 - 0
tracim/tracim/public/caldavzap/.htaccess 查看文件

@@ -0,0 +1,35 @@
1
+#########################################################################################################################
2
+# Apache configuration (REQUIRED for correct HTML5 cache functionality in browsers):
3
+# 1.) You NEED to enable the following Apache modules: mod_mime, mod_headers (optionally you can also enable mod_deflate)
4
+# 2.) You NEED to add the following lines into your Apache vhost configuration (without the # character):
5
+#     <Directory /client/installation/directory/>
6
+#        AllowOverride FileInfo Limit
7
+#         <IfVersion >= 2.3>
8
+#             Require all granted
9
+#         </IfVersion>
10
+#         <IfVersion < 2.3>
11
+#             Order allow,deny
12
+#             Allow from all
13
+#         </IfVersion>
14
+#     </Directory>
15
+#########################################################################################################################
16
+
17
+# Add "Content-Type: text/cache-manifest" header for .manifest files
18
+<IfModule mod_mime.c>
19
+    AddType text/cache-manifest .manifest
20
+</IfModule>
21
+
22
+# Add "Cache-Control: max-age=0, must-revalidate, no-cache, no-transform, private" header for all files
23
+#  for more information see: https://tools.ietf.org/html/rfc7234
24
+<IfModule mod_headers.c>
25
+    Header set Cache-Control "max-age=0, must-revalidate, no-cache, no-transform, private"
26
+</IfModule>
27
+
28
+<IfModule mod_deflate.c>
29
+    SetOutputFilter DEFLATE
30
+</IfModule>
31
+
32
+# If you use mod_cache set the correct path for the cache.manifest here
33
+#<IfModule mod_cache.c>
34
+#    CacheDisable cache.manifest
35
+#</IfModule>

+ 24 - 0
tracim/tracim/public/caldavzap/auth/.htaccess 查看文件

@@ -0,0 +1,24 @@
1
+#####################################################################################################
2
+# Apache configuration (REQUIRED to prevent access for .inc files /especially config files/)
3
+# You NEED to add the following lines into your Apache vhost configuration (without the # character):
4
+# <Directory /client/installation/directory/auth/>
5
+#    AllowOverride Limit
6
+#     <IfVersion >= 2.3>
7
+#         Require all granted
8
+#     </IfVersion>
9
+#     <IfVersion < 2.3>
10
+#         Order allow,deny
11
+#         Allow from all
12
+#     </IfVersion>
13
+# </Directory>
14
+#####################################################################################################
15
+
16
+<Files ~ "\.inc$">
17
+	<IfVersion >= 2.3>
18
+		Require all granted
19
+	</IfVersion>
20
+	<IfVersion < 2.3>
21
+		Order allow,deny
22
+		Deny from all
23
+	</IfVersion>
24
+</Files>

+ 41 - 0
tracim/tracim/public/caldavzap/auth/common.inc 查看文件

@@ -0,0 +1,41 @@
1
+<?php
2
+	function array_to_xml($array, $skip_top_closing=false, $level=0)
3
+	{
4
+		static $result="<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
5
+
6
+		foreach($array as $k => $v)
7
+		{
8
+			if(is_numeric($k))
9
+				array_to_xml($v, $skip_top_closing, $level);
10
+			else
11
+			{
12
+				for($j=0; $j<$level; $j++)
13
+					$result.="	";
14
+
15
+				$result.="<".htmlspecialchars($k);
16
+				if($k=='resources')
17
+					$result.=" xmlns=\"urn:com.inf-it:configuration\"";
18
+				if($v=='')
19
+					$result.=" />\n";
20
+				else
21
+				{
22
+					$result.=">";
23
+
24
+					if(is_array($v))
25
+					{
26
+						$result.="\n";
27
+						array_to_xml($v, $skip_top_closing, $level+1);
28
+						for($j=0; $j<$level; $j++)
29
+							$result.="	";
30
+					}
31
+					else
32
+						$result.=htmlspecialchars($v);
33
+
34
+					if($level!==0 || $skip_top_closing===false)
35
+						$result.="</".htmlspecialchars($k).">\n";
36
+				}
37
+			}
38
+		}
39
+		return $result;
40
+	}
41
+?>

+ 58 - 0
tracim/tracim/public/caldavzap/auth/config.inc 查看文件

@@ -0,0 +1,58 @@
1
+<?php
2
+	// auth method: generic (auth/plugins/generic_conf.inc) or ldap (auth/plugins/ldap_conf.inc)
3
+	$config['auth_method']='generic';
4
+
5
+	// set to true for debugging XML response, otherwise set to false to avoid browser
6
+	//  to show http authentication window after unsuccessful authentication
7
+	$config['auth_send_authenticate_header']=false;
8
+
9
+	// successfull authentication XML specification (change the "http://www.server.com:80" to your protocol/server/port)
10
+	$config['accounts']=array('resources'=>array());
11
+
12
+	// note: if you want to use regex values, then use one of the following formats (the second example is with regex modifier): 're:.*someregex.*[0-9]$' or 're|i:.*someregex.*[0-9]$'
13
+	// note: 'crossdomain' and 'withcredentials' are still available but there is NO REASON to use them (crossDomain is detected automatically, and I've never seen anyone who understand when to use withCredentials /there is NO REASON to set it to true!/)
14
+	// note: 'syncinterval' was removed - use globalSyncResourcesInterval in config.js instead
15
+	$config['accounts']['resources'][]=array(
16
+		'resource'=>array(
17
+			'type'=>array('calendar'=>''),
18
+			'href'=>(empty($_SERVER['HTTPS']) ? 'http' : 'https').'://www.server.com:80/caldav.php/'.$_SERVER['PHP_AUTH_USER'].'/',
19
+			'hreflabel'=>'null',		// if undefined or empty href value is used (see above)
20
+			'forcereadonly'=>'null',	// see auth/doc/example_config_response.xml for proper use, for example: 'forcereadonly'=>array(array('collection'=>'/caldav.php/user/collection/'), array('collection'=>'re:^/caldav.php/user/collection[0-9]/$')),
21
+			'settingsaccount'=>'true',	// client properties are saved here (note: set it to true only for ONE account)
22
+			'checkcontenttype'=>'true',	// check content-type in the server response (if you cannot see data in the interface /buggy server response/ you may try to disable it)
23
+			'delegation'=>'true',		// see auth/doc/example_config_response.xml for proper use, for example: 'delegation'=>array(array('resource'=>'/caldav.php/user%40domain.com/'), array('resource'=>'re|i:^/caldav.php/a[b-x].+/$')),
24
+			'ignorealarms'=>'false',	// see auth/doc/example_config_response.xml for proper use, for example: 'ignorealarms'=>array(array('collection'=>'/caldav.php/user/collection/'), array('collection'=>'re:^/caldav.php/user/collection[0-9]/$')),
25
+			'backgroundcalendars'=>'',	// see auth/doc/example_config_response.xml for proper use, for example: 'backgroundcalendars'=>array(array('collection'=>'/caldav.php/user/collection/'), array('collection'=>'re|i:^/caldav.php/user/collection[0-9]/$')),
26
+			'userauth'=>array(
27
+				'username'=>$_SERVER['PHP_AUTH_USER'],
28
+				'password'=>$_SERVER['PHP_AUTH_PW']
29
+			),
30
+			'timeout'=>90000,
31
+			'locktimeout'=>10000
32
+		)
33
+	);
34
+
35
+/*
36
+	// additional accounts
37
+	$config['accounts']['resources'][]=array(
38
+		'resource'=>array(
39
+			'type'=>array('calendar'=>''),
40
+			'href'=>'http://www.server.com:80/caldav.php/resource/',
41
+			'hreflabel'=>'null',		// if undefined or empty href value is used (see above)
42
+			'forcereadonly'=>'null',	// see auth/doc/example_config_response.xml for proper use, for example: 'forcereadonly'=>array(array('collection'=>'/caldav.php/user/collection/'), array('collection'=>'re:^/caldav.php/user/collection[0-9]/$')),
43
+			'settingsaccount'=>'false',	// client properties are saved here (note: set it to true only for ONE account)
44
+			'checkcontenttype'=>'true',	// check content-type in the server response (if you cannot see data in the interface /buggy server response/ you may try to disable it)
45
+			'delegation'=>'true',		// see auth/doc/example_config_response.xml for proper use, for example: 'delegation'=>array(array('resource'=>'/caldav.php/user%40domain.com/'), array('resource'=>'re|i:^/caldav.php/a[b-x].+/$')),
46
+			'ignorealarms'=>'false',	// see auth/doc/example_config_response.xml for proper use, for example: 'ignorealarms'=>array(array('collection'=>'/caldav.php/user/collection/'), array('collection'=>'re:^/caldav.php/user/collection[0-9]/$')),
47
+			'backgroundcalendars'=>'',	// see auth/doc/example_config_response.xml for proper use, for example: 'backgroundcalendars'=>array(array('collection'=>'/caldav.php/user/collection/'), array('collection'=>'re|i:^/caldav.php/user/collection[0-9]/$')),
48
+			'userauth'=>array(
49
+				'username'=>$_SERVER['PHP_AUTH_USER'],
50
+				'password'=>$_SERVER['PHP_AUTH_PW']
51
+			),
52
+			'timeout'=>90000,
53
+			'locktimeout'=>10000
54
+		)
55
+	);
56
+*/
57
+
58
+?>

+ 14 - 0
tracim/tracim/public/caldavzap/auth/cross_domain.inc 查看文件

@@ -0,0 +1,14 @@
1
+<?php
2
+	header_remove('Access-Control-Allow-Origin');
3
+	header_remove('Access-Control-Allow-Methods');
4
+	header_remove('Access-Control-Allow-Headers');
5
+	header_remove('Access-Control-Allow-Credentials');
6
+
7
+	header('Access-Control-Allow-Origin: *');
8
+	header('Access-Control-Allow-Methods: GET');
9
+	header('Access-Control-Allow-Headers: User-Agent,Authorization,Content-type,X-client');
10
+	header('Access-Control-Allow-Credentials: true');
11
+
12
+	if($_SERVER['REQUEST_METHOD']=='OPTIONS')	// Preflighted request
13
+		exit(0);
14
+?>

+ 85 - 0
tracim/tracim/public/caldavzap/auth/doc/example_config_response.xml 查看文件

@@ -0,0 +1,85 @@
1
+<resources xmlns="urn:com.inf-it:configuration">
2
+	<resource>
3
+		<type>
4
+			<calendar />
5
+		</type>
6
+		<href>http://www.server.com:8080/principals/users/user/</href>
7
+		<hreflabel></hreflabel>
8
+		<crossdomain>null</crossdomain>
9
+		<forcereadonly>null</forcereadonly>
10
+		<withcredentials>false</withcredentials>
11
+		<settingsaccount>true</settingsaccount>
12
+		<checkcontenttype>true</checkcontenttype>
13
+		<delegation>true</delegation>
14
+		<ignorealarms>false</ignorealarms>
15
+		<backgroundcalendars />
16
+		<userauth>
17
+			<username>user</username>
18
+			<password>password</password>
19
+		</userauth>
20
+		<timeout>90000</timeout>
21
+		<locktimeout>10000</locktimeout>
22
+	</resource>
23
+	<resource>
24
+		<type>
25
+			<calendar />
26
+		</type>
27
+		<href>http://www.server2.com:80/caldav.php/user/</href>
28
+		<hreflabel></hreflabel>
29
+		<crossdomain>null</crossdomain>
30
+		<forcereadonly>true</forcereadonly>
31
+		<withcredentials>false</withcredentials>
32
+		<settingsaccount>false</settingsaccount>
33
+		<checkcontenttype>true</checkcontenttype>
34
+		<delegation>
35
+			<resource>/caldav.php/user/</resource>
36
+			<resource>/principals/users/user%40domain.com/</resource>
37
+			<resource>re:^/caldav.php/a[b-x].+/$</resource>
38
+			<resource>re|i:^/caldav.php/a[b-x].+/$</resource>
39
+		</delegation>
40
+		<ignorealarms>
41
+			<collection>/caldav.php/user/collection/</collection>
42
+			<collection>/caldav.php/user%40domain.com/collection/</collection>
43
+			<collection>re:^/caldav.php/user/collection[0-9]/$</collection>
44
+			<collection>re|i:^/caldav.php/user/collection[0-9]/$</collection>
45
+		</ignorealarms>
46
+		<backgroundcalendars>
47
+			<collection>/caldav.php/user/collection/</collection>
48
+			<collection>/caldav.php/user%40domain.com/collection/</collection>
49
+			<collection>re:^/caldav.php/user/collection[0-9]/$</collection>
50
+			<collection>re|i:^/caldav.php/user/collection[0-9]/$</collection>
51
+		</backgroundcalendars>
52
+		<userauth>
53
+			<username>user</username>
54
+			<password>password</password>
55
+		</userauth>
56
+		<timeout>90000</timeout>
57
+		<locktimeout>10000</locktimeout>
58
+	</resource>
59
+	<resource>
60
+		<type>
61
+			<calendar />
62
+		</type>
63
+		<href>https://www.server3.com:8443/caldav.php/user/</href>
64
+		<hreflabel></hreflabel>
65
+		<crossdomain>null</crossdomain>
66
+		<forcereadonly>
67
+			<collection>/caldav.php/user/collection/</collection>
68
+			<collection>/caldav.php/user%40domain.com/collection/</collection>
69
+			<collection>re:^/caldav.php/user/collection[0-9]/$</collection>
70
+			<collection>re|i:^/caldav.php/user/collection[0-9]/$</collection>
71
+		</forcereadonly>
72
+		<withcredentials>false</withcredentials>
73
+		<settingsaccount>false</settingsaccount>
74
+		<checkcontenttype>true</checkcontenttype>
75
+		<delegation>false</delegation>
76
+		<ignorealarms>false</ignorealarms>
77
+		<backgroundcalendars />
78
+		<userauth>
79
+			<username>user</username>
80
+			<password>password</password>
81
+		</userauth>
82
+		<timeout>90000</timeout>
83
+		<locktimeout>10000</locktimeout>
84
+	</resource>
85
+</resources>

+ 7 - 0
tracim/tracim/public/caldavzap/auth/doc/readme.txt 查看文件

@@ -0,0 +1,7 @@
1
+1.) configure your auth method (see the plugins directory) and the response XML in auth/config.inc and set $config['auth_send_authenticate_header']=true
2
+2.) configure the selected auth module in plugins/PLUGIN_conf.inc
3
+3.) check the correct response by visiting http://your-server.com/client_dir/auth/ and entering username and password
4
+4.) set $config['auth_send_authenticate_header']=false in auth/config.inc
5
+
6
+By default the generic plugin is used for basic HTTP authentication ($config['auth_method']='generic'; in config.inc).
7
+

+ 33 - 0
tracim/tracim/public/caldavzap/auth/index.php 查看文件

@@ -0,0 +1,33 @@
1
+<?php
2
+	require_once('config.inc');
3
+	require_once('common.inc');
4
+	require_once('cross_domain.inc');
5
+	require_once('plugins/'.$config['auth_method'].'.inc');	// configured module - it defines the 'MODULE_authenticate()' function
6
+
7
+	if(call_user_func($config['auth_method'].'_authenticate')!==1)
8
+	{
9
+		// HTTP authentication (exit if unsuccessfull)
10
+		if($config['auth_send_authenticate_header'])
11
+			header('WWW-Authenticate: Basic realm="Inf-IT Auth Module"');
12
+		header('HTTP/1.0 401 Unauthorized');
13
+echo <<<HTML
14
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
15
+<html>
16
+	<head>
17
+		<title>401 Authorization Required</title>
18
+	</head>
19
+	<body>
20
+		<h1>Authorization Required</h1>
21
+		<p>This server could not verify that you are authorized to access the document requested. Either you supplied the wrong credentials (e.g., bad password), or your browser doesn't understand how to supply the credentials required.</p>
22
+	</body>
23
+</html>
24
+HTML;
25
+		exit(0);
26
+	}
27
+	else
28
+	{
29
+		header('Content-type: text/xml; charset="utf-8"');
30
+		header('Cache-Control: max-age=0, must-revalidate, no-cache, no-store, no-transform, private');
31
+		echo array_to_xml($config['accounts']);
32
+	}
33
+?>

+ 58 - 0
tracim/tracim/public/caldavzap/auth/plugins/generic.inc 查看文件

@@ -0,0 +1,58 @@
1
+<?php
2
+	require_once(dirname(__FILE__).'/generic_conf.inc');
3
+
4
+	function generic_authenticate()
5
+	{
6
+		global $pluginconfig;
7
+		if($_SERVER['PHP_AUTH_USER']!='' && $_SERVER['PHP_AUTH_PW']!='')
8
+		{
9
+			preg_match('#(https?)://([^/:]+)((?::[0-9]+)?)#i', $pluginconfig['base_url'], $matches);
10
+			$hostname_clean=$matches[2];
11
+			if($matches[1]=='https')
12
+				$hostname='ssl://'.$matches[2];
13
+			else
14
+				$hostname=$matches[2];
15
+
16
+			if($matches[3]=='')
17
+			{
18
+				if($matches[1]=='http')
19
+					$port=80;
20
+				else if($matches[1]=='https')
21
+					$port=443;
22
+			}
23
+			else
24
+				$port=substr($matches[3],1);
25
+
26
+			$fp=fsockopen($hostname, $port, $errno, $errstr, $pluginconfig['timeout']);
27
+			if(!$fp)
28
+			{
29
+				echo "$errstr ($errno)<br />\n";
30
+				return -2;
31
+			}
32
+			else
33
+			{
34
+				$request="<?xml version=\"1.0\" encoding=\"utf-8\"?><A:propfind xmlns:A=\"DAV:\"><A:prop><A:current-user-principal/></A:prop></A:propfind>";
35
+
36
+				$out="PROPFIND ".$pluginconfig['request']." HTTP/1.1\r\n";
37
+				$out.="Host: $hostname_clean\r\n";
38
+				$out.="Authorization: Basic ".base64_encode($_SERVER['PHP_AUTH_USER'].':'.$_SERVER['PHP_AUTH_PW'])."\r\n";
39
+				$out.="Depth: 0\r\n";
40
+				$out.="Content-Type: text/xml; charset=\"utf-8\"\r\n";
41
+				$out.="Content-Length:". strlen($request)."\r\n\r\n";
42
+				$out.=$request;
43
+				fwrite($fp, $out);
44
+
45
+				$result='';
46
+				if(!feof($fp))
47
+		    		$result.=fgets($fp);
48
+				fclose($fp);
49
+
50
+				if(strpos($result, 'HTTP/1.1 207')===0)
51
+					return 1;	// auth successful
52
+				else
53
+					return -1;	// auth unsuccessful
54
+			}
55
+		}
56
+		return 0;	// empty username or password
57
+	}
58
+?>

+ 12 - 0
tracim/tracim/public/caldavzap/auth/plugins/generic_conf.inc 查看文件

@@ -0,0 +1,12 @@
1
+<?php
2
+	// Server base URL
3
+	$pluginconfig['base_url']=(empty($_SERVER['HTTPS']) ? 'http' : 'https').'://my.server.com:8080';
4
+
5
+	// Default values are usually OK
6
+	//  for Davical:
7
+	$pluginconfig['request']='/caldav.php';	// change only if your Davical is not installed into server root directory
8
+	//  for Lion server:
9
+	//$pluginconfig['request']='/principals/users';
10
+
11
+	$pluginconfig['timeout']=30;
12
+?>

+ 37 - 0
tracim/tracim/public/caldavzap/auth/plugins/ldap.inc 查看文件

@@ -0,0 +1,37 @@
1
+<?php
2
+	require_once(dirname(__FILE__).'/ldap_conf.inc');
3
+
4
+	function ldap_authenticate()
5
+	{
6
+		global $pluginconfig;
7
+		if($_SERVER['PHP_AUTH_USER']!="" && $_SERVER['PHP_AUTH_PW']!="")
8
+		{
9
+			$ds=ldap_connect($pluginconfig['host']);
10
+
11
+			// if binding is required for LDAP search
12
+			if(isset($pluginconfig['bind_dn']) && isset($pluginconfig['bind_passwd']))
13
+			{
14
+				@ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);
15
+				if(!($r=@ldap_bind($ds, $pluginconfig['bind_dn'], $pluginconfig['bind_passwd'])))
16
+					return -2;	// auth unsuccessful (bind error)
17
+			}
18
+
19
+			// perform the search
20
+			if(($r=ldap_search($ds, $pluginconfig['basedn'], '(&('.$pluginconfig['user_attr'].'='.$_SERVER['PHP_AUTH_USER'].')'.(isset($pluginconfig['filter']) && $pluginconfig['filter']!='' ? '('.$pluginconfig['filter'].')' : '' ).')'))!==false)
21
+			{
22
+				$result=@ldap_get_entries($ds, $r);
23
+				if($result[0])
24
+				{
25
+					@ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);
26
+					if(@ldap_bind($ds, $result[0]['dn'], $_SERVER['PHP_AUTH_PW']))
27
+					{
28
+						@ldap_unbind($bi);
29
+						return 1;	// auth successful
30
+					}
31
+				}
32
+			}
33
+			return -1;	// auth unsuccessful
34
+		}
35
+		return 0;	// empty username or password
36
+	}
37
+?>

+ 12 - 0
tracim/tracim/public/caldavzap/auth/plugins/ldap_conf.inc 查看文件

@@ -0,0 +1,12 @@
1
+<?php
2
+	// LDAP configuration parameters
3
+	$pluginconfig['host']='ldaps://ldap.server.com/';
4
+	$pluginconfig['basedn']='ou=People,dc=server,dc=com';
5
+	$pluginconfig['user_attr']='uid';
6
+	// if the server requires binding (if set to null then binding is not performed)
7
+	//$pluginconfig['bind_dn']=null;
8
+	//$pluginconfig['bind_passwd']=null;
9
+
10
+	// optional
11
+	$pluginconfig['filter']='accountStatus=active';
12
+?>

+ 150 - 0
tracim/tracim/public/caldavzap/cache.manifest 查看文件

@@ -0,0 +1,150 @@
1
+CACHE MANIFEST
2
+#V 20160728145557
3
+
4
+CACHE:
5
+common.js
6
+config.js
7
+data_process.js
8
+forms.js
9
+interface.js
10
+localization.js
11
+main.js
12
+resource.js
13
+timezones.js
14
+vcalendar.js
15
+vcalendar_rfc_regex.js
16
+vtodo.js
17
+webdav_protocol.js
18
+css/default.css
19
+css/default_integration.css
20
+css/fullcalendar.css
21
+css/jquery-ui.custom.css
22
+css/spectrum.custom.css
23
+fonts/Roboto-BoldItalic-webfont.eot
24
+fonts/Roboto-BoldItalic-webfont.svg
25
+fonts/Roboto-BoldItalic-webfont.ttf
26
+fonts/Roboto-BoldItalic-webfont.woff
27
+fonts/Roboto-Bold-webfont.eot
28
+fonts/Roboto-Bold-webfont.svg
29
+fonts/Roboto-Bold-webfont.ttf
30
+fonts/Roboto-Bold-webfont.woff
31
+fonts/Roboto-Italic-webfont.eot
32
+fonts/Roboto-Italic-webfont.svg
33
+fonts/Roboto-Italic-webfont.ttf
34
+fonts/Roboto-Italic-webfont.woff
35
+fonts/Roboto-LightItalic-webfont.eot
36
+fonts/Roboto-LightItalic-webfont.svg
37
+fonts/Roboto-LightItalic-webfont.ttf
38
+fonts/Roboto-LightItalic-webfont.woff
39
+fonts/Roboto-Light-webfont.eot
40
+fonts/Roboto-Light-webfont.svg
41
+fonts/Roboto-Light-webfont.ttf
42
+fonts/Roboto-Light-webfont.woff
43
+fonts/Roboto-MediumItalic-webfont.eot
44
+fonts/Roboto-MediumItalic-webfont.svg
45
+fonts/Roboto-MediumItalic-webfont.ttf
46
+fonts/Roboto-MediumItalic-webfont.woff
47
+fonts/Roboto-Medium-webfont.eot
48
+fonts/Roboto-Medium-webfont.svg
49
+fonts/Roboto-Medium-webfont.ttf
50
+fonts/Roboto-Medium-webfont.woff
51
+fonts/Roboto-Regular-webfont.eot
52
+fonts/Roboto-Regular-webfont.svg
53
+fonts/Roboto-Regular-webfont.ttf
54
+fonts/Roboto-Regular-webfont.woff
55
+images/add_cal.svg
56
+images/add_cal_white.svg
57
+images/arrow_next_red.svg
58
+images/arrow_next.svg
59
+images/arrow_prev_red.svg
60
+images/arrow_prev.svg
61
+images/banner_calendar.svg
62
+images/banner_logout.svg
63
+images/banner_refresh.svg
64
+images/banner_todo.svg
65
+images/calendarB.svg
66
+images/cdz_logo.svg
67
+images/cloud.svg
68
+images/delegation.svg
69
+images/dp_left.svg
70
+images/dp_right.svg
71
+images/error_badge.svg
72
+images/error_b.svg
73
+images/error_w.svg
74
+images/in_progress_b.svg
75
+images/in_progress_dr.svg
76
+images/in_progress_r.svg
77
+images/in_progress_w.svg
78
+images/jumper_bottom_b.svg
79
+images/jumper_bottom_w.svg
80
+images/jumper_top_b.svg
81
+images/jumper_top_w.svg
82
+images/loadinfo.gif
83
+images/loadinfo_s1.gif
84
+images/loadinfo_s2.gif
85
+images/loadinfo_s3.gif
86
+images/loadinfo_s4.gif
87
+images/login.svg
88
+images/logout.svg
89
+images/needs_action_b.svg
90
+images/needs_action_dr.svg
91
+images/needs_action_r.svg
92
+images/needs_action_w.svg
93
+images/new_item.svg
94
+images/popupArrow.svg
95
+images/priority-1-dr.svg
96
+images/priority-1-r.svg
97
+images/priority-1.svg
98
+images/priority-1-w.svg
99
+images/priority-2-dr.svg
100
+images/priority-2-r.svg
101
+images/priority-2.svg
102
+images/priority-2-w.svg
103
+images/priority-3-dr.svg
104
+images/priority-3-r.svg
105
+images/priority-3.svg
106
+images/priority-3-w.svg
107
+images/read_only_b.svg
108
+images/read_only_w.svg
109
+images/remove_cal.svg
110
+images/remove_cal_white.svg
111
+images/reset_b.svg
112
+images/reset_dr.svg
113
+images/reset_drw.svg
114
+images/reset_r.svg
115
+images/reset_rw.svg
116
+images/reset_w.svg
117
+images/resource_arrow_down.svg
118
+images/resource_arrow_right.svg
119
+images/resource_arrow_up.svg
120
+images/resources.svg
121
+images/search.svg
122
+images/searchWhiteNew.svg
123
+images/select_bg_black.svg
124
+images/select_bg_dis.svg
125
+images/select_bg.svg
126
+images/select_black.svg
127
+images/select_dis.svg
128
+images/select_login.svg
129
+images/select.svg
130
+images/success_b.svg
131
+images/success_dr.svg
132
+images/success_drw.svg
133
+images/success_r.svg
134
+images/success_rw.svg
135
+images/success_w.svg
136
+images/todoB.svg
137
+lib/fullcalendar.js
138
+lib/ie_base64.js
139
+lib/jquery-2.1.4.min.js
140
+lib/jquery.autosize.js
141
+lib/jquery.browser.js
142
+lib/jquery.placeholder-1.1.9.js
143
+lib/jquery.quicksearch.js
144
+lib/jquery-ui-1.11.4.custom.js
145
+lib/jshash-2.2_sha256.js
146
+lib/rrule.js
147
+lib/spectrum.js
148
+
149
+NETWORK:
150
+*

+ 79 - 0
tracim/tracim/public/caldavzap/cache_handler.js 查看文件

@@ -0,0 +1,79 @@
1
+// OFFLINE CACHE DEBUGGING
2
+
3
+/*var cacheStatusValues=[];
4
+cacheStatusValues[0]='uncached';
5
+cacheStatusValues[1]='idle';
6
+cacheStatusValues[2]='checking';
7
+cacheStatusValues[3]='downloading';
8
+cacheStatusValues[4]='updateready';
9
+cacheStatusValues[5]='obsolete';
10
+
11
+var cache=window.applicationCache;
12
+cache.addEventListener('cached', logEvent, false);
13
+cache.addEventListener('checking', logEvent, false);
14
+cache.addEventListener('downloading', logEvent, false);
15
+cache.addEventListener('error', logEvent, false);
16
+cache.addEventListener('noupdate', logEvent, false);
17
+cache.addEventListener('obsolete', logEvent, false);
18
+cache.addEventListener('progress', logEvent, false);
19
+cache.addEventListener('updateready', logEvent, false);
20
+
21
+function logEvent(e)
22
+{
23
+	var online, status, type, message;
24
+	online=(navigator.onLine) ? 'yes' : 'no';
25
+	status=cacheStatusValues[cache.status];
26
+	type=e.type;
27
+	message='online: '+online;
28
+	message+=', event: '+type;
29
+	message+=', status: '+status;
30
+	if(type=='error' && navigator.onLine)
31
+		message+=' (prolly a syntax error in manifest)';
32
+	console.log(message);
33
+}
34
+
35
+window.applicationCache.addEventListener('updateready', function(){
36
+		window.applicationCache.swapCache();
37
+		console.log('swap cache has been called');
38
+	}, false
39
+);
40
+
41
+//setInterval(function(){cache.update()}, 10000);*/
42
+
43
+// Check if a new cache is available on page load.
44
+window.addEventListener('load', function(e)
45
+{
46
+	window.applicationCache.addEventListener('cached', function(e)
47
+	{
48
+		if(!isUserLogged)
49
+			window.location.reload();
50
+		else
51
+			$('#cacheDialog').css('display','block');
52
+	}, false);
53
+
54
+	window.applicationCache.addEventListener('updateready', function(e)
55
+	{
56
+		if(!isUserLogged)
57
+			window.location.reload();
58
+		else
59
+			$('#cacheDialog').css('display','block');
60
+	}, false);
61
+
62
+	window.applicationCache.addEventListener('obsolete', function(e)
63
+	{
64
+		if(!isUserLogged)
65
+			window.location.reload();
66
+		else
67
+			$('#cacheDialog').css('display','block');
68
+	}, false);
69
+
70
+	window.applicationCache.addEventListener('noupdate', function(e)
71
+	{
72
+		if(!isUserLogged)
73
+		{
74
+			clearInterval(globalCacheUpdateInterval);
75
+			globalCacheUpdateInterval=setInterval(function(){window.applicationCache.update();}, 300000);
76
+			//$('#LoginPage .window').css('display', 'inline-block');
77
+		}
78
+	}, false);
79
+}, false);

+ 5 - 0
tracim/tracim/public/caldavzap/cache_update.sh 查看文件

@@ -0,0 +1,5 @@
1
+#! /bin/bash
2
+# Use this script every time you modify any file to force browsers to reload it (empty HTML5 cache).
3
+
4
+command -v ed &> /dev/null || { echo "Error: 'ed' not installed. Aborting." > /dev/stderr; exit 1; }
5
+printf ",s/#V.*/#V $(date '+%Y%m%d%H%M%S')/\nw\nq\n" | ed -s cache.manifest

+ 294 - 0
tracim/tracim/public/caldavzap/changelog.txt 查看文件

@@ -0,0 +1,294 @@
1
+CalDavZAP Changelog
2
+
3
+
4
+NOTE: if you are interested in integrated version of CalDavZAP and CardDavMATE (our CardDAV web client) please use InfCloud - http://www.inf-it.com/open-source/clients/infcloud/
5
+
6
+version 0.13.1 [2015-09-22]:
7
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
8
+- changed login screen autocomplete behaviour - we do not prevent browsers from remembering login/password anymore
9
+
10
+version 0.13.0 [2015-09-16]:
11
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
12
+- note: if you use a server with cross-domain setup see the modified Access-Control-Allow-Headers and Access-Control-Expose-Headers in readme.txt (or misc/config_davical.txt or misc/calendarserver.diff); you MUST update these headers, otherwise the client will NOT work
13
+- configuration - added globalEnableRefresh option which enables/disables the new "refresh all resources" icon in the left application menu (disabled by default)
14
+- added Chinese localization (zh_CN) - thanks Fandy
15
+- added shift+login shortcut to ignore settings stored on the server and use the default settings (this functionality was added long time ago, but I forgot to mention about it)
16
+- added support for "Prefer: return=representation" (and related "Preference-Applied: return=representation") for PUT requests (see http://tools.ietf.org/html/rfc7240); this change REQUIRES update of Access-Control-Allow-Headers and Access-Control-Expose-Headers if cross-domain setup is used
17
+- added full RFC2445 support - RRULE processing is now performed by rrule.js (see: https://github.com/jakubroztocil/rrule); thanks to this library we now support/expand all recurrences, although the most exotic ones are "read-only" (for these you will see "Other (modification not supported)" in the interface)
18
+- added DESCRIPTION property for VALARM components to make them RFC compliant
19
+- added check for unsupported XML 1.0 characters in user entered data - these are replaced by a space character (to prevent client and/or server side parsing errors)
20
+- added title with version number for the software name/description (login screen)
21
+- added vCalendar line folding (RFC2445 - section 4.1)
22
+- fixed event processing when multiple VEVENT and VTIMEZONE components are intermingled
23
+- fixed VTODO COMPLETED property (UTC time format)
24
+- fixed alarm window not being localized properly
25
+- fixed an occasional issue where all collections are double loaded on login
26
+- changed version checking - use internal build number for software version comparison to support update notification also for beta and rc builds
27
+- changed format and comments in config.js
28
+- changed storing of user settings (PROPPATCH request) - no server request will be made if there is no change in settings
29
+- updated jQuery to 2.1.4
30
+- updated localizations - thanks Niels Bo Andersen [da_DK], Marten Gajda [de_DE], Damian Vila [es_ES], Gabriela Vattier [fr_FR], Luca Ferrario [it_IT], Muimu Nakayama [ja_JP], Johan Vromans [nl_NL], Selcuk Pultar [tr_TR], Александр Симонов [ru_RU], Serge Yakimchuck [uk_UA]
31
+- updated timezone.js to latest IANA timezone database
32
+- other improvements and fixes
33
+
34
+version 0.12.1 [2015-03-16]:
35
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
36
+- note: you NEED to enable "mod_headers" in Apache (for other servers see your server documentation) which is used to generate proper HTTP headers (required for correct support of HTML5 cache in browsers); the previously used mod_expire is not longer used (see the changelog entry below)
37
+- added support for absolute collection URLs returned in PROPFIND request
38
+- fixed HTML5 cache related problems (especially in Firefox) by returning "Cache-Control: max-age=0, must-revalidate, no-cache, no-transform, private" header instead of "Cache-Control: max-age=0" - this fix requires enabled "mod_headers" (you can disable the previously used "mod_expires") in Apache - for more details see .htaccess
39
+- fixed processing of the language parameter in the title of event/todo
40
+- changed displaying of event/todo calendar list in event/todo form - now it is possible to create new event/todo also into inactive event/todo collection
41
+- updated jQuery-UI to 1.11.4
42
+- other improvements and fixes
43
+
44
+version 0.12.0 [2015-01-26]:
45
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
46
+- configuration - added globalDefaultEventDuration configuration variable - set the default duration (in minutes) for newly created events
47
+- added widened todo list with dynamic number of columns
48
+- added checkboxes for todos in the todo list - now you can change the status of a todo by clicking on its checkbox
49
+- added duplicate button for copying of events/todos
50
+- fixed loading of future/past todos - now the loading of additional future/past todos is performed also by clicking on datepicker calendar (in the todo list view)
51
+- updated jQuery to 2.1.3
52
+- updated jQuery-UI to 1.11.2
53
+- other improvements and fixes
54
+
55
+version 0.11.1 [2014-10-07]:
56
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
57
+- updated timezone.js to latest IANA timezone database
58
+- updated jQuery to 2.1.1
59
+- updated jQuery-UI to 1.11.1
60
+- fixed calendar color change functionality
61
+- other improvements and fixes
62
+
63
+version 0.11.0 [2014-10-02]:
64
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
65
+- note: this release contains new, changed and also removed configuration options (always use the latest config.js)
66
+- configuration - removed showHeader option from globalAccountSettings, globalNetworkCheckSettings and globalNetworkAccountSettings - it is incompatible with new functionality
67
+- configuration - added globalCrossServerSettingsURL configuration option - enable this option if your CalDavZAP installation is accessible from multiple URLs (URL1, URL2), otherwise settings (such as enabled/active collections) stored from the URL1 will be incompatible with settings stored from URL2
68
+- configuration - added globalCalendarColorPropertyXmlns configuration option - used to define the namespace for calendar-color property (see below)
69
+- configuration - changed default value for delegation option to true (in globalAccountSettings, globalNetworkCheckSettings and globalNetworkAccountSettings)
70
+- configuration - changed default value for globalEventStartPastLimit and globalEventStartFutureLimit from 2 to 3
71
+- added Japan localization (ja_JP) - thanks Muimu Nakayama
72
+- added support for loading and unloading of user collections and delegated collections (delegation functionality)
73
+- added support for calendar color change (write support for calendar-color property)
74
+- added arrow icons for agenda views to indicate out of view events
75
+- added currently logged user into the page title
76
+- added hover element for calendar events
77
+- updated localizations - thanks Michael Rasmussen [da_DK], Marten Gajda [de_DE], Damián Vila [es_ES], Jean-Christophe Bach [fr_FR], Luca Ferrario [it_IT], Johan Vromans [nl_NL], Selcuk Pultar [tr_TR], Александр Симонов [ru_RU], Yevgen Martsenyuk [uk_UA]
78
+- fixed occasional wrong UID processing when moving events/todos between different calendar collections
79
+- fixed issues with subscribed calendars
80
+- fixed processing of alarms
81
+- fixed an occasional parseDate bug due to daylight saving time in specific timezones
82
+- various fixes, optimalizations, improvements, visual updates and more
83
+
84
+version 0.10.0.5 [2014-04-14]:
85
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
86
+- updated Russian localization (ru_RU)
87
+- fixed wrong processing of RECURRENCE-ID property in UTC (Z) timezone
88
+- fixed occasional wrong processing of repeating events generated in future
89
+
90
+version 0.10.0.4 [2014-03-15]:
91
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
92
+- added Russian localization (ru_RU) - thanks Александр Симонов
93
+- fixed synchronization of removed events for servers without sync-collection report support
94
+- minor translation fixes
95
+
96
+version 0.10.0.3 [2014-03-12]:
97
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
98
+- added support for LDAP binding in auth/ldap module (see auth/plugins/ldap_conf.inc)
99
+- fixed occasional wrong processing of DTEND attribute
100
+- fixed incorrect creation of recurring events which caused that multiple different UIDs can be present in one calendar object (edit + save of previously created events will split them into multiple objects)
101
+
102
+version 0.10.0.2 [2014-02-17]:
103
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
104
+- added Ukrainian localization (uk_UA) - thanks Serge Yakimchuck
105
+- added misc/readme_baikal_sabredav.txt and misc/baikal-flat-0.2.7.diff to solve issues related to storing CalDavZAP properties on SabreDAV and Baïkal - thanks Johannes Zellner
106
+- fixed invalid XML response processing (SabreDAV and Baïkal)
107
+- fixed invalid XML request if globalEventStartPastLimit and globalEventStartFutureLimit are set to null
108
+- fixed "delegation" XML processing
109
+- updated French [fr_FR] localization - thanks Jean-Christophe Bach
110
+
111
+version 0.10.0.1 [2014-02-04]:
112
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
113
+- added Spanish localization (es_ES) - thanks Damián Vila
114
+- updated jQuery to 2.1.0
115
+- updated jQuery-UI to 1.10.4
116
+- changed various default date formats
117
+- changed alarm behaviour - it is no longer possible to create multiple identical alarms (they are automatically merged into one)
118
+- fixed a visual bug when displaying a simple todo alert
119
+- fixed rare issue where UNTIL attribute of recurrent events was not processed correctly
120
+
121
+version 0.10.0 [2014-01-22]:
122
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
123
+- note: this release contains new, changed and also removed configuration options (always use the latest config.js)
124
+- configuration - added globalEventStartPastLimit and globalEventStartFutureLimit for time-range filtering - note: for servers without time-range filtering support you need to set both variables to null (see config.js)
125
+- configuration - replaced globalInactiveCollections and globalInactiveTodoCollections by globalActiveCalendarCollections and globalActiveTodoCollections (see config.js)
126
+- configuration - removed globalResourceHeaderShowLogin option - it is replaced by much more flexible hrefLabel option in globalAccountSettings and globalNetworkCheckSettings (see config.js)
127
+- configuration - removed syncInterval option from globalAccountSettings and globalNetworkCheckSettings - detection of sync-token changes is now performed by ONE request instead of N (number of collections) - use globalSyncResourcesInterval instead
128
+- configuration - removed crossDomain and withCredentials options from default globalAccountSettings, globalNetworkCheckSettings and globalNetworkAccountSettings - both settings are still available but there is NO REASON to use them (crossDomain is detected automatically, and I've never seen anyone who understand when to use withCredentials /there is NO REASON to set it to true!/)
129
+- major improvements and changes in sychronization code - MUCH reduced number of HTTP request to server
130
+- major design changes (including the open source Roboto font)
131
+- major readme.txt update with detailed descriptions of most common setup problems
132
+- added support for time-range filtering (requires server with time-range filtering support) - EXTREME performance improvements
133
+- added workaround for buggy HTML5 cache handling in the latest Firefox
134
+- added "equivalency" for todo filters (globalAppleRemindersMode) - NEEDS-ACTION, IN-PROGRESS and CANCELLED are processed as NEEDS-ACTION
135
+- added calendar color indicator for event/todo forms (unified with the upcoming CardDavMATE)
136
+- added handling of unsupported settings
137
+- added support for 'headervalue' collection property (namespace: http://inf-it.com/ns/dav/) - useful for collection grouping
138
+- added new overlay with refresh button, when cache manifest change is detected (it forces users to reload the page)
139
+- added support/mapping for alternative timezone names - e.g. 'US/Pacific' (legacy name) is mapped to 'America/Los_Angeles' (current name)
140
+- changed resource list design (unified with the upcoming CardDavMATE)
141
+- changed todo processing if globalAppleRemindersMode is enabled - todos with start and no end are processed as simple todos
142
+- changed displaying of repeating todo confirm question
143
+- changed time-range filtering for todos - all todos from future are loaded from server initially
144
+- updated French [fr_FR] localization - thanks Jean-Christophe Bach
145
+- optimized window resizing functionality
146
+- fixed local timezone processing
147
+- fixed loader hanging after login if subscribed calendar list is empty
148
+- fixed generating of repeating events in future
149
+- fixed RRULE processing if specified in YYYYMMDD format
150
+- fixed displaying of arrows for repeating events
151
+- fixed sorting of resources
152
+- fixed various search issues
153
+- disabled opening of new event/todo form if only read-only collections are present
154
+- removed jQuery source mapping file reference
155
+- LOT of other improvements and fixes
156
+
157
+version 0.9.1.2 [2013-08-05]:
158
+- fixed processing of recurrent events (special recurrences - correct BYMONTHDAY processing)
159
+- fixed globalTimeFormatBasic configuration option processing (it is no longer ignored)
160
+- removed old and unused configuration options (globalDefaultDisplayTodo and globalTodoHideExpired)
161
+
162
+version 0.9.1.1 [2013-07-30]:
163
+- fixed processing of recurrent events with until date
164
+- fixed saving of until dates values in recurrent events
165
+- fixed parsing of double quoted TZID param values
166
+
167
+version 0.9.1 [2013-07-26]:
168
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
169
+- note: this release contains changed configuration options (always use the latest config.js)
170
+- configuration - changed globalAppleRemindersMode option values - newly supported values are iOS6, iOS7, true (it is set to latest supported iOS - in this case iOS7) and false (see config.js)
171
+- added Turkish localization (tr_TR) - thanks Selcuk Pultar
172
+- updated localizations - thanks Michael Rasmussen [da_DK], Marten Gajda [de_DE], John Fischer [fr_FR], Luca Ferrario [it_IT] and Johan Vromans [nl_NL]
173
+- added additional automatic fixes for invalid events
174
+- added support for STATUS attribute
175
+- added support for CALSCALE attribute (only GREGORIAN is supported; missing attribute = GREGORIAN)
176
+- added automatic change of "time to" after "time from" change (preserve the event/todo duration)
177
+- added support for dynamic height of NOTE field - thanks http://www.jacklmoore.com/autosize/
178
+- fixed problem with always visible completed todos when globalAppleRemindersMode enabled
179
+- fixed window resize callback
180
+- fixed incorrect detection of privileges for binded resources
181
+- fixed processing of RECURRENCE-ID in events/todos
182
+- fixed parsing of todo/event components with same UID in subscribed calendars
183
+- fixed parsing of due date timezone
184
+- fixed processing of DURATION value for allday events
185
+- fixed problem with multiple URL and LOCATION attributes
186
+- fixed handling of VERSION attribute
187
+- fixed repeating todo and event processing
188
+- fixed timezone picker problems
189
+- updated jQuery to 2.0.3
190
+- changed default "due date" for todos to date selected in the todo calendar
191
+- other improvements and fixes
192
+
193
+version 0.9.0 [2013-06-27]:
194
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache); alternatively you can update the cache.manifest manually - edit the second line beginning with "#V 20" to anything else (this file simple needs "some" change)
195
+- note: if you use DAViCal with cross-domain setup see the modified Apache configuration in misc/config_davical.txt (added Access-Control-Expose-Headers header)
196
+- note: if you use OS X Calendarserver it is recommended to re-patch your installation (added Access-Control-Expose-Headers header; see misc/readme_osx.txt)
197
+- note: this release contains new and also changed configuration options (always use the latest config.js)
198
+- configuration - added globalSettingsType option - set the destination for client settings on server (useful if your server not allows to store properties to "principal-URL" but allows to store them to "calendar-home-set")
199
+- configuration - added checkContentType option into globalAccountSettings and globalNetworkCheckSettings - enables content-type checking for server response (only objects with proper content-type are inserted into interface) - if you cannot see data in the interface you may try to disable it
200
+- configuration - added globalAppleRemindersMode option (enabled by default) - it enables workarounds for Apple clients (see config.js)
201
+- configuration - added globalIgnoreCompletedAlarms option (enabled by default) - it disables alarm for completed todos (see config.js)
202
+- MAJOR performance improvements
203
+- added support for Cyrus server - thanks Ken Murchison
204
+- added support for additional CalDAV servers (should work with the same servers as CardDavMATE)
205
+- added completely new and shiny interface for todos
206
+- added support for additional todo properties and repeating todos
207
+- added support for PRODID property for both events and todos
208
+- added new custom formats for time and day strings based on currently selected localization
209
+- added Hungarian localization (hu_HU)
210
+- changed cache.manifest - cache all image files in HTML5 cache
211
+- changed ordering of calendars in selectbox (globalSortAlphabet is used)
212
+- changed internal logic of resource loading, synchronization and version check functionality (to prepare for integration with CardDavMATE)
213
+- changed minimum height of events to height of "30 minutes" event
214
+- fixed Firefox placeholder colors
215
+- fixed and updated various localization strings
216
+- fixed events and todos sometimes being editable even with forceReadOnly flag enabled
217
+- fixed various timezone processing issues
218
+- fixed visual event form bug when using repeat option with weekend/business days
219
+- fixed current time indicator error during day/week transition
220
+- fixed timezone picker (at the bottom of the resource list) - it is no longer editable using keyboard navigation while editing event/todo
221
+- fixed wrong ajax parameter which may cause warnings in server log
222
+- fixed an issue when timezone picker was not always visible after login
223
+- updated left menu with new icons (thanks Kelecsenyi Timotej - http://timotejos.com/)
224
+- updated jQuery to 2.0.2 (and related fixes)
225
+- updated jQuery-UI to 1.10.3 (and related fixes)
226
+- updated auth module to reflect the latest changes in configuration options
227
+- updated misc directory (it is the same as in CardDavMATE)
228
+- updated localizations - thanks Marten Gajda [de_DE], John Fischer [fr_FR], Luca Ferrario [it_IT] and Johan Vromans [nl_NL] (note: Danish [da_DK] localization contains some untranslated strings)
229
+- LOT of other improvements and fixes
230
+
231
+version 0.8.1.1 [2013-02-25]:
232
+- fixed multiple bugs related to processing of recurrent events
233
+- fixed forced lower case problem of some strings in the interface
234
+- other minor fixes
235
+
236
+version 0.8.1 [2013-02-21]:
237
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache)
238
+- added support for background calendars in day view event list
239
+- added missing misc directory
240
+- fixed syntax error if configured using globalAccountSettings
241
+- fixed issues with delegation proccessing if multiple accounts are configured
242
+- fixed issue with pinned tab in Firefox (manual reloading is not required anymore)
243
+- fixed privileges for binded calendars - these are now strictly read only
244
+- fixed wrong proccessing of number of occurrences for recurrent events
245
+- fixed issue where readonly events could still be edited via drag and drop and resizing
246
+- fixed and optimized the "Revert" button functionality
247
+- fixed issue with saving empty URL property
248
+- fixed incorrect alarm box position
249
+- fixed bad positioning of error image for "repeat end" date field
250
+
251
+version 0.8.0 [2013-02-13]:
252
+- note: do not forget to execute the cache_update.sh script every time you update your configuration or any other file (otherwise your browser will use the previous version of files stored in HTML5 cache)
253
+- note: this release contains new configuration option (always use the latest config.js)
254
+- configuration - added globalUseJqueryAuth option - use jQuery .ajax() auth or custom header for HTTP basic auth (default); set this option to true if your server uses digest auth (note: you may experience auth popups on some browsers)
255
+- configuration - added globalRemoveUnknownTimezone (disabled by default) - it removes non-standard/unknown timezones if event/todo is edited (and saved)
256
+- configuration - added delegation option into globalAccountSettings and globalNetworkCheckSettings (sets additional delegated resources - if true then delegation is enabled for all available resources; if false (default) then delegation is disabled; if an array of URL encoded resources or regexes (for example: ['/caldav.php/user/', '/caldav.php/user%40domain.com/', new RegExp('^/caldav.php/a[b-x].+/$', 'i')] then delegation is enabled for all specified resources
257
+- configuration - added ignoreAlarms option into globalAccountSettings and globalNetworkCheckSettings (defines an array calendars with disabled alarm - if true then all alarms are ignored; if false (default) then alarms are enabled; if an array of URL encoded collections or regexes (for example: ['/caldav.php/user/collection/', '/caldav.php/user%40domain.com/collection/', new RegExp('^/caldav.php/user/collection[0-9]/$', 'i')] then alarm is disabled for all specified resources
258
+- configuration - added backgroundCalendars option into globalAccountSettings and globalNetworkCheckSettings - defines an array of background calendars - if there is at least one event defined for the given day in a background calendar, the background color for that day will be pink/light-red; to use this feature define an array of URL encoded collections or regexes (for example: ['/caldav.php/user/collection/', '/caldav.php/user%40domain.com/collection/', new RegExp('^/caldav.php/user/collection[0-9]/$', 'i')])
259
+- configuration - added user defined time format support for events via globalTimeFormatBasic and globalTimeFormatAgenda variables (see config.js)
260
+- configuration - changed forceReadonly property proccessing - URL encoded collections and also regexes are now supported (see config.js)
261
+- configuration - changed globalCalendarSelected variable proccessing - full UID (for example: http://username@domain.com:8080/caldav.php/user/calendar/) and also UID matching regexes are now supported (see config.js)
262
+- configuration - date and time formats are now predefined for each localization - if you want to use custom date and time formats instead of predefined formats (defined by localizations) use globalAMPMFormat and globalDatepickerFormat variables (commented out by default)
263
+- added Danish localization (da_DK) - thanks Niels Bo Andersen
264
+- added German localization (de_DE) - thanks Marten Gajda and Thomas Scheel
265
+- added Italian localization (it_IT) - thanks Luca Ferrario
266
+- added French localization (fr_FR) - thanks John Fischer
267
+- added Dutch localization (nl_NL) - thanks Johan Vromans
268
+- added additional functionality for today button - now it scrolls the calendar to ensure that the today slot is visible in the top of the view
269
+- added support for fallback to PROPFIND if REPORT is not supported and server returns incorrect 403 error code (instead of 400 or 501)
270
+- added support for events without DTEND or DURATION values
271
+- added support for DURATION property
272
+- added support for CLASS property (Privacy)
273
+- added support for TRANSP property (Availability)
274
+- added support for URL property
275
+- updated timezone.js to latest IANA timezone database
276
+- updated auth module to reflect the latest changes in configuration options
277
+- changed button label from "All future events" to "This and all future events" for more clarity
278
+- changed the "repeat end" option text from "after" to "occurences" for more clarity (event ends after X occurences, including the first one)
279
+- changed event listing in day view - now it scrolls to the very top if the currently displayed day is the first day of month (the button for loading the previous month is now visible)
280
+- changed event listing in day view - now it scrolls to the closest following day if the currently displayed day is not found (no events exist for that day)
281
+- fixed login => logout => relogin as different user bug
282
+- fixed "Unable to save" bug when creating/editing an event/todo
283
+- fixed cache_update.sh - replaced sed by ed due to cross OS compatibility problems
284
+- fixed duplicate scrollbar problem in week and day views
285
+- fixed useless revert button - it is no longer visible when creating a new event or todo
286
+- fixed position of the error image in todo completed field
287
+- fixed processing of UNTIL values in repeating events
288
+- fixed EXDATE value processing and saving
289
+- fixed January specific bug
290
+- fixed BYMONTH value processing - anniversaries
291
+- other improvements and fixes
292
+
293
+version 0.7.0 [2012-11-20]:
294
+- initial public release

+ 628 - 0
tracim/tracim/public/caldavzap/common.js 查看文件

@@ -0,0 +1,628 @@
1
+/*
2
+CalDavZAP - the open source CalDAV Web Client
3
+Copyright (C) 2011-2015
4
+    Jan Mate <jan.mate@inf-it.com>
5
+    Andrej Lezo <andrej.lezo@inf-it.com>
6
+    Matej Mihalik <matej.mihalik@inf-it.com>
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
10
+published by the Free Software Foundation, either version 3 of the
11
+License, or (at your option) any later version.
12
+
13
+This program is distributed in the hope that it will be useful,
14
+but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
+GNU Affero General Public License for more details.
17
+
18
+You should have received a copy of the GNU Affero General Public License
19
+along with this program. If not, see <http://www.gnu.org/licenses/>.
20
+*/
21
+
22
+// Used to match XML element names with any namespace
23
+jQuery.fn.filterNsNode=function(nameOrRegex)
24
+{
25
+	return this.filter(
26
+		function()
27
+		{
28
+			if(nameOrRegex instanceof RegExp)
29
+				return (this.nodeName.match(nameOrRegex) || this.nodeName.replace(RegExp('^[^:]+:',''),'').match(nameOrRegex));
30
+			else
31
+				return (this.nodeName===nameOrRegex || this.nodeName.replace(RegExp('^[^:]+:',''),'')===nameOrRegex);
32
+		}
33
+	);
34
+};
35
+
36
+// Escape jQuery selector
37
+function jqueryEscapeSelector(inputValue)
38
+{
39
+	return (inputValue==undefined ? '' : inputValue).toString().replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g,'\\$1');
40
+}
41
+
42
+// Generate random string (UID)
43
+function generateUID()
44
+{
45
+	uidChars='0123456789abcdefghijklmnopqrstuvwxyz';
46
+	UID='';
47
+	for(i=0;i<32;i++)
48
+	{
49
+		if(i==8 || i==12 || i==16 || i==20) UID+='-';
50
+		UID+=uidChars.charAt(Math.floor(Math.random()*(uidChars.length-1)));
51
+	}
52
+	return UID;
53
+}
54
+
55
+
56
+// IE compatibility
57
+if (typeof window.btoa=='undefined' && typeof base64.encode!='undefined') window.btoa=base64.encode;
58
+
59
+// Create Basic auth string (for HTTP header)
60
+function basicAuth(user, password)
61
+{
62
+	var tok=user+':'+password;
63
+	var hash=btoa(tok);
64
+	return 'Basic '+hash;
65
+}
66
+
67
+// multiply regex replace {'regex': value, 'regex': value}
68
+String.prototype.multiReplace=function(hash)
69
+{
70
+	var str=this, key;
71
+	for(key in hash)
72
+		str=str.replace(new RegExp(key,'g'), hash[key]);
73
+	return str;
74
+};
75
+
76
+// Used for sorting the contact and resource list ...
77
+String.prototype.customCompare=function(stringB, alphabet, dir, caseSensitive)
78
+{
79
+	var stringA=this;
80
+
81
+	if(alphabet==undefined || alphabet==null)
82
+		return stringA.localeCompare(stringB);
83
+	else
84
+	{
85
+		var pos=0,
86
+		min=Math.min(stringA.length, stringB.length);
87
+		dir=dir || 1;
88
+		caseSensitive=caseSensitive || false;
89
+		if(!caseSensitive)
90
+		{
91
+			stringA=stringA.toLowerCase();
92
+			stringB=stringB.toLowerCase();
93
+		}
94
+		while(stringA.charAt(pos)===stringB.charAt(pos) && pos<min){pos++;}
95
+
96
+		if(stringA.charAt(pos)=='')
97
+			return -dir;
98
+		else
99
+		{
100
+			var index1=alphabet.indexOf(stringA.charAt(pos));
101
+			var index2=alphabet.indexOf(stringB.charAt(pos));
102
+
103
+			if(index1==-1 || index2==-1)
104
+				return stringA.localeCompare(stringB);
105
+			else
106
+				return (index1<index2 ? -dir : dir);
107
+		}
108
+	}
109
+};
110
+
111
+function customResourceCompare(objA, objB)
112
+{
113
+	return objA.displayValue.customCompare(objB.displayValue, globalSortAlphabet, 1, false);
114
+}
115
+
116
+function checkColorBrightness(hex)
117
+{
118
+	var R=parseInt(hex.substring(0, 2), 16);
119
+	var G=parseInt(hex.substring(2, 4), 16);
120
+	var B=parseInt(hex.substring(4, 6), 16);
121
+	return Math.sqrt(0.241*R*R+0.691*G*G+0.068*B*B);
122
+}
123
+
124
+// Get unique values from array
125
+Array.prototype.unique=function()
126
+{
127
+	var o={}, i, l=this.length, r=[];
128
+	for(i=0;i<l;i++)
129
+		o[this[i]]=this[i];
130
+	for(i in o)
131
+		r.push(o[i]);
132
+	return r;
133
+};
134
+
135
+// Recursive replaceAll
136
+String.prototype.replaceAll=function(stringToFind,stringToReplace)
137
+{
138
+	var temp=this;
139
+	while(temp.indexOf(stringToFind)!=-1)
140
+		temp=temp.replace(stringToFind,stringToReplace);
141
+	return temp;
142
+};
143
+
144
+// Pad number with leading zeroes
145
+Number.prototype.pad=function(size){
146
+	var s=String(this);
147
+	while(s.length<size)
148
+		s='0'+s;
149
+	return s;
150
+};
151
+
152
+// Case insensitive search for attributes
153
+// Usage:	$('#selector').find(':attrCaseInsensitive(data-type,"'+typeList[i]+'")')
154
+jQuery.expr[':'].attrCaseInsensitive=function(elem, index, match)
155
+{
156
+	var matchParams=match[3].split(','),
157
+		attribute=matchParams[0].replace(/^\s*|\s*$/g,''),
158
+		value=matchParams[1].replace(/^\s*"|"\s*$/g,'').toLowerCase();
159
+	return jQuery(elem)['attr'](attribute)!=undefined && jQuery(elem)['attr'](attribute)==value;
160
+};
161
+
162
+// Capitalize given string
163
+function capitalize(string)
164
+{
165
+	return string.charAt(0).toUpperCase()+string.slice(1).toLowerCase();
166
+}
167
+var timezoneKeys = new Array();
168
+function populateTimezoneKeys()
169
+{
170
+	for(var i in timezones)
171
+	timezoneKeys.push(i);
172
+
173
+	timezoneKeys.push('0local');
174
+	timezoneKeys.push('1UTC');
175
+
176
+	timezoneKeys.sort();
177
+
178
+	timezoneKeys[0] = timezoneKeys[0].substring(1);
179
+	timezoneKeys[1] = timezoneKeys[1].substring(1);
180
+	jQuery.extend(timezones,{'UTC':{}});
181
+}
182
+
183
+Date.prototype.getWeekNo=function()
184
+{
185
+	var today = this;
186
+	Year = today.getFullYear();
187
+	Month = today.getMonth();
188
+	Day = today.getDate();
189
+	now = Date.UTC(Year,Month,Day,0,0,0);
190
+	var Firstday = new Date();
191
+	Firstday.setYear(Year);
192
+	Firstday.setMonth(0);
193
+	Firstday.setDate(1);
194
+	then = Date.UTC(Year,0,1,0,0,0);
195
+	var Compensation = Firstday.getDay();
196
+	if(((now-then)/86400000) > 3)
197
+		NumberOfWeek =  Math.round((((now-then)/86400000)+Compensation)/7);
198
+	else
199
+	{
200
+		if(Firstday.getDay()>4 || Firstday.getDay()==0)
201
+		NumberOfWeek =  53;
202
+	}
203
+	return NumberOfWeek;
204
+}
205
+
206
+function zeroPad(n) {
207
+	return (n < 10 ? '0' : '') + n;
208
+}
209
+
210
+var dateFormatters = {
211
+	s	: function(d)	{return d.getSeconds() },
212
+	ss	: function(d)	{return zeroPad(d.getSeconds())},
213
+	m	: function(d)	{return d.getMinutes()},
214
+	mm	: function(d)	{return zeroPad(d.getMinutes())},
215
+	h	: function(d)	{return d.getHours() % 12 || 12},
216
+	hh	: function(d)	{return zeroPad(d.getHours() % 12 || 12)},
217
+	H	: function(d)	{return d.getHours()},
218
+	HH	: function(d)	{return zeroPad(d.getHours())},
219
+	d	: function(d)	{return d.getDate()},
220
+	dd	: function(d)	{return zeroPad(d.getDate())},
221
+	ddd	: function(d,o)	{return o.dayNamesShort[d.getDay()]},
222
+	dddd: function(d,o)	{return o.dayNames[d.getDay()]},
223
+	W	: function(d)	{return d.getWeekNo()},
224
+	M	: function(d)	{return d.getMonth() + 1},
225
+	MM	: function(d)	{return zeroPad(d.getMonth() + 1)},
226
+	MMM	: function(d,o)	{return o.monthNamesShort[d.getMonth()]},
227
+	MMMM: function(d,o)	{return o.monthNames[d.getMonth()]},
228
+	yy	: function(d)	{return (d.getFullYear()+'').substring(2)},
229
+	yyyy: function(d)	{return d.getFullYear()},
230
+	t	: function(d)	{return d.getHours() < 12 ? 'a' : 'p'},
231
+	tt	: function(d)	{return d.getHours() < 12 ? 'am' : 'pm'},
232
+	T	: function(d)	{return d.getHours() < 12 ? 'A' : 'P'},
233
+	TT	: function(d)	{return d.getHours() < 12 ? 'AM' : 'PM'},
234
+	u	: function(d)	{return formatDates(d, null, "yyyy-MM-dd'T'HH:mm:ss'Z'")},
235
+	S	: function(d)	{
236
+		var date = d.getDate();
237
+		if (date > 10 && date < 20) {
238
+			return 'th';
239
+		}
240
+		return ['st', 'nd', 'rd'][date%10-1] || 'th';
241
+	}
242
+};
243
+
244
+
245
+function formatDates(date1, date2, format, options) {
246
+	options = options;
247
+	var date = date1,
248
+		otherDate = date2,
249
+		i, len = format.length, c,
250
+		i2, formatter,
251
+		res = '';
252
+	for (i=0; i<len; i++) {
253
+		c = format.charAt(i);
254
+		if (c == "'") {
255
+			for (i2=i+1; i2<len; i2++) {
256
+				if (format.charAt(i2) == "'") {
257
+					if (date) {
258
+						if (i2 == i+1) {
259
+							res += "'";
260
+						}else{
261
+							res += format.substring(i+1, i2);
262
+						}
263
+						i = i2;
264
+					}
265
+					break;
266
+				}
267
+			}
268
+		}
269
+		else if (c == '(') {
270
+			for (i2=i+1; i2<len; i2++) {
271
+				if (format.charAt(i2) == ')') {
272
+					var subres = formatDates(date, null, format.substring(i+1, i2), options);
273
+					if (parseInt(subres.replace(/\D/, ''), 10)) {
274
+						res += subres;
275
+					}
276
+					i = i2;
277
+					break;
278
+				}
279
+			}
280
+		}
281
+		else if (c == '[') {
282
+			for (i2=i+1; i2<len; i2++) {
283
+				if (format.charAt(i2) == ']') {
284
+					var subformat = format.substring(i+1, i2);
285
+					var subres = formatDates(date, null, subformat, options);
286
+					if (subres != formatDates(otherDate, null, subformat, options)) {
287
+						res += subres;
288
+					}
289
+					i = i2;
290
+					break;
291
+				}
292
+			}
293
+		}
294
+		else if (c == '{') {
295
+			date = date2;
296
+			otherDate = date1;
297
+		}
298
+		else if (c == '}') {
299
+			date = date1;
300
+			otherDate = date2;
301
+		}
302
+		else {
303
+			for (i2=len; i2>i; i2--) {
304
+				if (formatter = dateFormatters[format.substring(i, i2)]) {
305
+					if (date) {
306
+						res += formatter(date, options);
307
+					}
308
+					i = i2 - 1;
309
+					break;
310
+				}
311
+			}
312
+			if (i2 == i) {
313
+				if (date) {
314
+					res += c;
315
+				}
316
+			}
317
+		}
318
+	}
319
+	return res;
320
+};
321
+function vObjectLineFolding(inputText)
322
+{
323
+	var outputText='';
324
+	var maxLineOctetLength=75;
325
+	var count=0;
326
+
327
+	for(var i=0; inputText[i]!=undefined; i++)
328
+	{
329
+		var currentChar=inputText.charCodeAt(i);
330
+		var nextChar=inputText.charCodeAt(i+1);
331
+		if(currentChar==0x000D && nextChar==0x000A)
332
+		{
333
+			count=0;
334
+			outputText+='\r\n';
335
+			i++;
336
+			continue;
337
+		}
338
+
339
+		var surrogatePair=false;
340
+		if(currentChar<0x0080)
341
+			var charNum=1;
342
+		else if(currentChar<0x0800)
343
+			var charNum=2;
344
+		else if(currentChar<0xd800 || currentChar>=0xe000)
345
+			var charNum=3;
346
+		else
347
+		{
348
+			// surrogate pair
349
+			// UTF-16 encodes 0x10000-0x10FFFF by subtracting 0x10000 and splitting
350
+			// the 20 bits of 0x0-0xFFFFF into two halves
351
+			charNum=4;
352
+			surrogatePair=true;
353
+		}
354
+
355
+		if(count>maxLineOctetLength-charNum)
356
+		{
357
+			outputText+='\r\n ';
358
+			count=1;
359
+		}
360
+		outputText+=String.fromCharCode(currentChar);
361
+		if(surrogatePair)
362
+		{
363
+			outputText+=String.fromCharCode(vCardText.charCodeAt(i+1));
364
+			i++;
365
+		}
366
+		count+=charNum;
367
+	}
368
+	return outputText;
369
+}
370
+
371
+function rgbToHex(rgb)
372
+{
373
+	rgb=rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d*)?|(?:\.\d+)))?\)$/);
374
+	function hex(x)
375
+	{
376
+		return ("0"+parseInt(x).toString(16)).slice(-2);
377
+	}
378
+	return "#"+hex(rgb[1])+hex(rgb[2])+hex(rgb[3]);
379
+}
380
+
381
+function hexToRgba(hex, transparency) {
382
+	var bigint=parseInt(hex.substring(1), 16);
383
+	var r=(bigint >> 16) & 255;
384
+	var g=(bigint >> 8) & 255;
385
+	var b=bigint & 255;
386
+
387
+	return 'rgba('+r+','+g+','+b+','+transparency+')';
388
+}
389
+
390
+function rgbToRgba(rgb, transparency)
391
+{
392
+	rgb=rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d*)?|(?:\.\d+)))?\)$/);
393
+	return 'rgba('+rgb[1]+','+rgb[2]+','+rgb[3]+','+transparency+')';
394
+}
395
+
396
+function dataGetChecked(resourceListSelector)
397
+{
398
+	var checkedArr=$(resourceListSelector).find('input[type=checkbox]:checked').not('.unloadCheck').filter('[data-id]').filter(function(){return this.indeterminate==false}).map(function(){return $(this).attr('data-id')}).get();
399
+
400
+	for(i=checkedArr.length-1; i>=0; i--)
401
+		if(checkedArr[i].match(new RegExp('[^/]$'))!=null && checkedArr.indexOf(checkedArr[i].replace(new RegExp('[^/]+$'), ''))!=-1)
402
+			checkedArr.splice(i, 1);
403
+
404
+	return checkedArr;
405
+}
406
+
407
+function resourceChBoxClick(obj, resourceListSelector, headerSelector, returnChecked)
408
+{
409
+	$(obj).parent().nextUntil(headerSelector).find('input[type=checkbox]:visible').prop('checked', $(obj).prop('checked')).prop('indeterminate', false);
410
+	if(returnChecked)
411
+		return dataGetChecked(resourceListSelector);
412
+}
413
+
414
+function collectionChBoxClick(obj, resourceListSelector, headerSelector, collectionSelector, groupSelector, returnChecked)
415
+{
416
+	if(collectionSelector.match('_item$'))
417
+	{
418
+		var tmp_coh=$(obj).parent().prevAll(headerSelector).first();
419
+		var tmp_co_chbxs=tmp_coh.nextUntil(headerSelector).find('input[type=checkbox]:visible');
420
+	}
421
+	else
422
+	{
423
+		var tmp_coh=$(obj).parent().parent().prevAll(headerSelector).first();
424
+		var tmp_co_chbxs=tmp_coh.nextUntil(headerSelector).find(collectionSelector).find('input[type=checkbox]:visible');
425
+	}
426
+
427
+	if(groupSelector!=null)
428
+	{
429
+		if($(obj).prop('checked')==false && $(obj).prop('indeterminate')==false && $(obj).attr('data-ind')=='false' &&
430
+		$(obj).parent().next(groupSelector).height()>0/* note: ':visible' is not working! */)
431
+		{
432
+			$(obj).prop('indeterminate', true);
433
+			$(obj).prop('checked', true);
434
+			$(obj).attr('data-ind', 'true');
435
+			tmp_coh.find('input[type=checkbox]:visible').prop('indeterminate', true).prop('checked', false);
436
+
437
+			if(returnChecked)
438
+				return dataGetChecked(resourceListSelector);
439
+			return true;
440
+		}
441
+		else if($(obj).attr('data-ind')=='true')
442
+			$(obj).attr('data-ind', 'false');
443
+
444
+		$(obj).parent().next(groupSelector).find('input[type=checkbox]').prop('checked', $(obj).prop('checked'));
445
+	}
446
+
447
+	if(tmp_co_chbxs.length==tmp_co_chbxs.filter(':checked').length)
448
+		tmp_coh.find('input[type=checkbox]:visible').prop('checked', true).prop('indeterminate', false);
449
+	else if(tmp_co_chbxs.filter(':checked').length==0 && tmp_co_chbxs.filter(function(){return this.indeterminate==true}).length==0)
450
+		tmp_coh.find('input[type=checkbox]:visible').prop('checked', false).prop('indeterminate', false);
451
+	else
452
+		tmp_coh.find('input[type=checkbox]:visible').prop('indeterminate', true).prop('checked', false);
453
+
454
+	if(returnChecked)
455
+		return dataGetChecked(resourceListSelector);
456
+}
457
+
458
+function groupChBoxClick(obj, resourceListSelector, headerSelector, collectionSelector, groupSelector, returnChecked)
459
+{
460
+	var tmp_cg=$(obj).closest(groupSelector);
461
+	var tmp_cg_chbxs=tmp_cg.find('input[type=checkbox]:visible');
462
+	var tmp_co_chbxs=tmp_cg.prev().find('input[type=checkbox]:visible');
463
+
464
+	if(tmp_cg_chbxs.filter(':checked').length==0)
465
+		tmp_co_chbxs.prop('checked', false).prop('indeterminate', false);
466
+	else
467
+		tmp_co_chbxs.prop('indeterminate', true).prop('checked', false);
468
+
469
+	return collectionChBoxClick(tmp_co_chbxs, resourceListSelector, headerSelector, collectionSelector, null, returnChecked);
470
+}
471
+
472
+function loadResourceChBoxClick(obj, resourceListSelector, headerSelector, collectionSelector, resourceItemSelector)
473
+{
474
+	if(collectionSelector.match('_item$'))
475
+	{
476
+		var firstCollection=$(obj).parent().nextUntil(headerSelector).first();
477
+		if($(obj).prop('checked'))
478
+			$(obj).parent().nextUntil(headerSelector).addBack().removeClass('unloaded');
479
+		else
480
+			$(obj).parent().nextUntil(headerSelector).addBack().addClass('unloaded');
481
+	}
482
+	else
483
+	{
484
+		var firstCollection=$(obj).parent().nextUntil(headerSelector).first().find(collectionSelector);
485
+		if($(obj).prop('checked'))
486
+		{
487
+			$(obj).parent().nextUntil(headerSelector).find(collectionSelector).removeClass('unloaded');
488
+			$(obj).parent().removeClass('unloaded');
489
+		}
490
+		else
491
+		{
492
+			$(obj).parent().nextUntil(headerSelector).find(collectionSelector).addClass('unloaded');
493
+			$(obj).parent().addClass('unloaded');
494
+		}
495
+	}
496
+
497
+	$(resourceListSelector).find(headerSelector).find('.unloadCheckHeader:checked').prop('disabled',false);
498
+	$(resourceListSelector).find(collectionSelector).find('.unloadCheck:checked').prop('disabled',false);
499
+	if(!$(resourceListSelector).find(headerSelector).find('.unloadCheckHeader').filter(function(){return $(this).prop('checked') || $(this).prop('indeterminate');}).length)
500
+	{
501
+		$(obj).prop({'checked':false,'indeterminate':true});
502
+		$(obj).parent().removeClass('unloaded');
503
+		$(obj).parent().nextUntil(headerSelector).find('.unloadCheck').prop({'checked':false,'indeterminate':false});
504
+		firstCollection.removeClass('unloaded').find('.unloadCheck').prop({'checked':true,'indeterminate':false,'disabled':true});
505
+	}
506
+	else
507
+	{
508
+		$(obj).parent().nextUntil(headerSelector).find('.unloadCheck').prop({'checked':$(obj).prop('checked'),'indeterminate':false});
509
+		var checkedCollections=$(resourceListSelector).find(collectionSelector).find('.unloadCheck:checked');
510
+		if(checkedCollections.length==1)
511
+		{
512
+			var collection=checkedCollections.parents(resourceItemSelector);
513
+			if(!collection.prev().hasClass(resourceItemSelector.slice(1)) && !collection.next().hasClass(resourceItemSelector.slice(1)))
514
+				collection.prev().find('.unloadCheckHeader').prop('disabled',true);
515
+			checkedCollections.prop('disabled',true);
516
+		}
517
+	}
518
+}
519
+
520
+function loadCollectionChBoxClick(obj, resourceListSelector, headerSelector, collectionSelector, resourceItemSelector)
521
+{
522
+	if($(obj).prop('checked'))
523
+		$(obj).parent().removeClass('unloaded');
524
+	else
525
+		$(obj).parent().addClass('unloaded');
526
+
527
+	var checkedCollections=$(resourceListSelector).find(collectionSelector).find('.unloadCheck:checked');
528
+	if(checkedCollections.length==1)
529
+	{
530
+		var collection=checkedCollections.parents(resourceItemSelector);
531
+		if(!collection.prev().hasClass(resourceItemSelector.slice(1)) && !collection.next().hasClass(resourceItemSelector.slice(1)))
532
+			collection.prev().find('.unloadCheckHeader').prop('disabled',true);
533
+		checkedCollections.prop('disabled',true);
534
+	}
535
+	else
536
+	{
537
+		$(resourceListSelector).find(headerSelector).find('.unloadCheckHeader:checked').prop('disabled',false);
538
+		checkedCollections.prop('disabled',false);
539
+	}
540
+
541
+	if(collectionSelector.match('_item$'))
542
+	{
543
+		var tmp_coh=$(obj).parent().prevAll(headerSelector).first();
544
+		var tmp_co_chbxs=tmp_coh.nextUntil(headerSelector).find('.unloadCheck');
545
+	}
546
+	else
547
+	{
548
+		var tmp_coh=$(obj).parent().parent().prevAll(headerSelector).first();
549
+		var tmp_co_chbxs=tmp_coh.nextUntil(headerSelector).find(collectionSelector).find('.unloadCheck');
550
+	}
551
+
552
+	if(tmp_co_chbxs.length==tmp_co_chbxs.filter(':checked').length)
553
+		tmp_coh.removeClass('unloaded').find('.unloadCheckHeader').prop('checked', true).prop('indeterminate', false);
554
+	else if(tmp_co_chbxs.filter(':checked').length==0 && tmp_co_chbxs.filter(function(){return this.indeterminate==true}).length==0)
555
+		tmp_coh.addClass('unloaded').find('.unloadCheckHeader').prop('checked', false).prop('indeterminate', false);
556
+	else
557
+		tmp_coh.removeClass('unloaded').find('.unloadCheckHeader').prop('indeterminate', true).prop('checked', false);
558
+}
559
+
560
+// Escape vCalendar value - RFC2426 (Section 2.4.2)
561
+function vcalendarEscapeValue(inputValue)
562
+{
563
+	return (inputValue==undefined ? '' : inputValue).replace(vCalendar.pre['escapeRex'],"\\$1").replace(vCalendar.pre['escapeRex2'],'\\n');
564
+}
565
+
566
+// Unescape vCalendar value - RFC2426 (Section 2.4.2)
567
+function vcalendarUnescapeValue(inputValue)
568
+{
569
+	var outputValue='';
570
+
571
+	if(inputValue!=undefined)
572
+	{
573
+		for(var i=0;i<inputValue.length;i++)
574
+			if(inputValue[i]=='\\' && i+1<inputValue.length)
575
+			{
576
+				if(inputValue[++i]=='n')
577
+					outputValue+='\n';
578
+				else
579
+					outputValue+=inputValue[i];
580
+			}
581
+			else
582
+				outputValue+=inputValue[i];
583
+	}
584
+	return outputValue;
585
+}
586
+
587
+// Split parameters and remove double quotes from values (if parameter values are quoted)
588
+function vcalendarSplitParam(inputValue)
589
+{
590
+	var result=vcalendarSplitValue(inputValue, ';');
591
+	var index;
592
+
593
+	for(var i=0;i<result.length;i++)
594
+	{
595
+		index=result[i].indexOf('=');
596
+		if(index!=-1 && index+1<result[i].length && result[i][index+1]=='"' && result[i][result[i].length-1]=='"')
597
+			result[i]=result[i].substring(0,index+1)+result[i].substring(index+2,result[i].length-1);
598
+	}
599
+	return result;
600
+}
601
+
602
+// Split string by separator (but not '\' escaped separator)
603
+function vcalendarSplitValue(inputValue, inputDelimiter)
604
+{
605
+	var outputArray=new Array();
606
+	var i=0;
607
+	var j=0;
608
+
609
+	for(i=0;i<inputValue.length;i++)
610
+	{
611
+		if(inputValue[i]==inputDelimiter)
612
+		{
613
+			if(outputArray[j]==undefined)
614
+				outputArray[j]='';
615
+			++j;
616
+			continue;
617
+		}
618
+		outputArray[j]=(outputArray[j]==undefined ? '' : outputArray[j])+inputValue[i];
619
+		if(inputValue[i]=='\\' && i+1<inputValue.length)
620
+			outputArray[j]=outputArray[j]+inputValue[++i];
621
+	}
622
+	return outputArray;
623
+}
624
+
625
+function dateFormatJqToFc(input)
626
+{
627
+	return input.replaceAll('DD','dddd').replaceAll('D','ddd').replace(/(MM|M)/g, '$1MM').replaceAll('m','M').replace(/y/g,'yy');
628
+}

+ 931 - 0
tracim/tracim/public/caldavzap/config.js 查看文件

@@ -0,0 +1,931 @@
1
+/*
2
+CalDavZAP - the open source CalDAV Web Client
3
+Copyright (C) 2011-2015
4
+    Jan Mate <jan.mate@inf-it.com>
5
+    Andrej Lezo <andrej.lezo@inf-it.com>
6
+    Matej Mihalik <matej.mihalik@inf-it.com>
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
10
+published by the Free Software Foundation, either version 3 of the
11
+License, or (at your option) any later version.
12
+
13
+This program is distributed in the hope that it will be useful,
14
+but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
+GNU Affero General Public License for more details.
17
+
18
+You should have received a copy of the GNU Affero General Public License
19
+along with this program. If not, see <http://www.gnu.org/licenses/>.
20
+*/
21
+
22
+
23
+// NOTE: see readme.txt before you start to configure this client!
24
+
25
+
26
+// NOTE: do not forget to execute the cache_update.sh script every time you
27
+// update this configuration file or any other files (otherwise your browser
28
+// will use the previous version of files stored in HTML5 cache). Alternatively
29
+// you can update the cache.manifest manually - edit the second line beginning
30
+// with "#V 20" to anything else (this file simple needs "some" change)
31
+
32
+
33
+// Supported setup types (use ONE of them):
34
+//   a.) globalAccountSettings => username and password is hardcoded
35
+//       in config.js, automatic login without the login screen
36
+//       - advantages: fast login process = no username/password is required
37
+//       - disadvantages: username/password is visible in your config.js, so
38
+//         this type of setup is recommended ONLY for intranet/home users
39
+//   b.) globalNetworkCheckSettings => standard setup with login screen
40
+//       - advantages: username/password is required (no visible
41
+//         username/password in config.js)
42
+//       - disadvantages: if a user enters wrong username/password then
43
+//         the browser will show authentication popup window (it is NOT
44
+//         possible to disable it in JavaScript; see the next option)
45
+//   c.) globalNetworkAccountSettings => advanced setup with login screen
46
+//       - advantages: no authentication popup if you enter wrong username/
47
+//         password, dynamic XML configuration generator (you can generate
48
+//         different configurations for your users /by modifying the "auth"
49
+//         module configuration or the PHP code itself/)
50
+//       - disadvantages: requires PHP >= 5.3 and additional configuration,
51
+//         only basic http authentication is supported => always use https!
52
+//
53
+//
54
+// What is a "principal URL"? => Check you server documentation!
55
+//   - "principal URL" is NOT "collection URL"
56
+//   - this client automatically detects collections for "principal URL"
57
+//   - PROPER "principal URL" looks like:
58
+//     https://server.com:8443/principals/users/USER/
59
+//     https://server.com:8443/caldav.php/USER/
60
+//   - INVALID principal URL looks like:
61
+//     https://server.com:8443/principals/users/USER/collection/
62
+//       => this is a collection URL
63
+//     https://server.com:8443/caldav.php/USER/collection/
64
+//       => this is a collection URL
65
+//     https://server.com:8443/principals/users/USER
66
+//       => missing trailing '/'
67
+//     https://server.com:8443/caldav.php/USER
68
+//       => missing trailing '/'
69
+//     /caldav.php/USER/
70
+//       => relative URL instead of full URL
71
+//
72
+//
73
+// List of properties used in globalAccountSettings, globalNetworkCheckSettings
74
+// and globalNetworkAccountSettings variables (+ in the "auth" module):
75
+// - href
76
+//   Depending on the setup type set the value to:
77
+//   a.) globalAccountSettings: full "principal URL"
78
+//   b.) globalNetworkCheckSettings: "principal URL" WITHOUT the "USER/" part
79
+//   c.) globalNetworkAccountSettings: "full URL" to the "auth" directory
80
+//   This property is supported in:
81
+//     globalAccountSettings
82
+//     globalNetworkCheckSettings
83
+//     globalNetworkAccountSettings
84
+// - userAuth
85
+//   - userName
86
+//     Set the username you want to login.
87
+//   - userPassword
88
+//     Set the password for the given username.
89
+//   This property is supported in:
90
+//     globalAccountSettings
91
+// - timeOut
92
+//   This option sets the timeout for jQuery .ajax call (in miliseconds).
93
+//   Example:
94
+//     timeOut: 90000
95
+//   This property is supported in:
96
+//     globalAccountSettings
97
+//     globalNetworkCheckSettings
98
+//     globalNetworkAccountSettings
99
+// - lockTimeOut 
100
+//   NOTE: used only if server supports LOCK requests
101
+//   This option sets the LOCK timeout value if resource locking
102
+//   is used (in miliseconds).
103
+//   Example:
104
+//     lockTimeOut: 10000
105
+//   This property is supported in:
106
+//     globalAccountSettings
107
+//     globalNetworkCheckSettings
108
+//     globalNetworkAccountSettings (available in auth module only)
109
+// - checkContentType
110
+//   This option enables a content-type checking for server response.
111
+//   If enabled then only objects with proper content-type are inserted
112
+//   into the interface.
113
+//   If you cannot see data in the interface you may try to disable it (useful
114
+//   if your server returns wrong value in "propstat/prop/getcontenttype").
115
+//   If undefined then content-type checking is enabled.
116
+//   Examples:
117
+//     checkContentType: true
118
+//     checkContentType: false
119
+//   This property is supported in:
120
+//     globalAccountSettings
121
+//     globalNetworkCheckSettings
122
+//     globalNetworkAccountSettings (available in auth module only)
123
+// - settingsAccount
124
+//   NOTE: server support for custom DAV properties is REQUIRED!
125
+//   This option sets the account where the client properties such as:
126
+//   loaded collections, enabled collections, ... are saved during
127
+//   the logout and resource/collection synchronisation
128
+//   NOTE: set it to true ONLY for ONE account!
129
+//   Examples:
130
+//     settingsAccount: true
131
+//     settingsAccount: false
132
+//   This property is supported in:
133
+//     globalAccountSettings
134
+//     globalNetworkCheckSettings
135
+//     globalNetworkAccountSettings (available in auth module only)
136
+// - delegation
137
+//   NOTE: server support for this functionality is REQUIRED!
138
+//   This option allows you to load delegated (shared) collections.
139
+//   If set to true (default) then delegation functionality is enabled,
140
+//   and the interface allows you to load delegated collections.
141
+//   If false then delegation functionality is completely disabled.
142
+//   Examples:
143
+//     delegation: true
144
+//     delegation: false
145
+//   This property is supported in:
146
+//     globalAccountSettings
147
+//     globalNetworkCheckSettings
148
+//     globalNetworkAccountSettings (available in auth module only)
149
+// - additionalResources
150
+//   This options sets the list of additional resources (e.g. shared resources
151
+//   accessible by all users). If the server supports delegation (see
152
+//   the delegation option above) there is no reason to use this option!
153
+//   Supported values:
154
+//   - array of URL encoded resource names (not collections), such as:
155
+//     'company'
156
+//     'shared_resource'
157
+//   If empty (default) or undefined then shared resources are not loaded
158
+//   using this option, but may be loaded using the delegation option.
159
+//   Examples:
160
+//     additionalResources=[]
161
+//     additionalResources=['public', 'shared_resource']
162
+//   This property is supported in:
163
+//     globalNetworkCheckSettings
164
+// - hrefLabel
165
+//   This option sets the server name in the resource header (useful if
166
+//   you want to see custom resource header above the collections).
167
+//   You can use the following variables in the value:
168
+//     %H = full hostname (including the port number)
169
+//     %h = full hostname (without the port number)
170
+//     %D = full domain name
171
+//     %d = only the first and second level domain
172
+//     %P = principal name
173
+//     %p = principal name without the @domain.com part (if present)
174
+//     %U = logged user name
175
+//     %u = logged user name without the @domain.com part (if present)
176
+//   If undefined, empty or or null then '%d/%p [%u]' is used.
177
+//   Examples: 
178
+//     hrefLabel: '%d/%p [%u]'
179
+//     hrefLabel: '%D/%u'
180
+//   This property is supported in:
181
+//     globalAccountSettings
182
+//     globalNetworkCheckSettings
183
+//     globalNetworkAccountSettings (available in auth module only)
184
+// - forceReadOnly
185
+//   This option sets the list of collections as "read-only".
186
+//   Supported values:
187
+//   - true
188
+//     all collections will be "read-only"
189
+//   - array of URL encoded
190
+//     - collections, such as: 
191
+//       '/caldav.php/user/calendar/'
192
+//       '/caldav.php/user%40domain.com/calendar/'
193
+//     - regexes, such as:
194
+//       new RegExp('^/caldav.php/user/calendar[0-9]/$', 'i')
195
+//     specifies the list of collections marked as "read-only"
196
+//   If null (default) or undefined then server detected privileges are used.
197
+//   Examples:
198
+//     forceReadOnly: null
199
+//     forceReadOnly: true
200
+//     forceReadOnly: ['/caldav.php/user/calendar/', 
201
+//                     '/caldav.php/user/calendar2/']
202
+//     forceReadOnly: [new RegExp('^/.*/user/calendar[0-9]/$', 'i')]
203
+//   This property is supported in:
204
+//     globalAccountSettings
205
+//     globalNetworkCheckSettings
206
+//     globalNetworkAccountSettings (available in auth module only, with
207
+//       different syntax for regexes)
208
+// - ignoreAlarms
209
+//   This option sets list of calendar collections with disabled
210
+//   alarm functionality.
211
+//   Supported values:
212
+//   - true
213
+//     alarm functionality is disabled for all collections
214
+//   - array of URL encoded
215
+//     - collections, such as: 
216
+//       '/caldav.php/user/calendar/'
217
+//       '/caldav.php/user%40domain.com/calendar/'
218
+//     - regexes, such as:
219
+//       new RegExp('^/caldav.php/user/calendar[0-9]/$', 'i')
220
+//     specifies the list of collections with disabled alarm functionality.
221
+//   If false (default) or undefined then alarm functionality is enabled
222
+//   for all collections.
223
+//   Examples:
224
+//     ignoreAlarms: true
225
+//     ignoreAlarms: ['/caldav.php/user/calendar/', 
226
+//                    '/caldav.php/user/calendar2/']
227
+//     ignoreAlarms: [new RegExp('^/.*/user/calendar[0-9]/$', 'i')]
228
+//   This property is supported in:
229
+//     globalAccountSettings
230
+//     globalNetworkCheckSettings
231
+//     globalNetworkAccountSettings (available in auth module only, with
232
+//       different syntax for regexes)
233
+// - backgroundCalendars
234
+//   This options defines a list of background calendars. If there is
235
+//   at least one event defined for the given day in a background calendar,
236
+//   the background color for that day will be pink/light-red.
237
+//   Supported values:
238
+//   - array of URL encoded
239
+//     - collections, such as: 
240
+//       '/caldav.php/user/calendar/'
241
+//       '/caldav.php/user%40domain.com/calendar/'
242
+//     - regexes, such as:
243
+//       new RegExp('^/caldav.php/user/calendar[0-9]/$', 'i')
244
+//     specifies the list of background calendar collections.
245
+//   Examples:
246
+//     backgroundCalendars: ['/caldav.php/user/calendar/', 
247
+//                           '/caldav.php/user/calendar2/']
248
+//     backgroundCalendars: [new RegExp('^/.*/user/calendar[0-9]/$', 'i')]
249
+//   This property is supported in:
250
+//     globalAccountSettings
251
+//     globalNetworkCheckSettings
252
+//     globalNetworkAccountSettings (available in auth module only, with
253
+//       different syntax for regexes)
254
+// Special options not present in configuration examples:
255
+// NOTE: use ONLY if you know what are you doing!
256
+// - crossDomain
257
+//   This option sets the crossDomain for jQuery .ajax call. If null (default)
258
+//   then the value is autodetected /and the result is shown in the console/
259
+// - withCredentials
260
+//   This option sets the withCredentials for jQuery .ajax call. The default
261
+//   value is false and there is NO REASON to change it to true!
262
+//   NOTE: if true, Access-Control-Allow-Origin "*" (CORS header) not works!
263
+
264
+
265
+// globalAccountSettings
266
+// Use this option if you want to use automatic login (without a login
267
+// screen) with hardcoded username/password in config.js. Otherwise use
268
+// globalNetworkCheckSettings or globalNetworkAccountSettings (see below).
269
+// NOTE: if this option is used the value must be an array of object(s).
270
+// List of properties used in globalAccountSettings variable:
271
+// - href
272
+//   Set this option to the full "principal URL".
273
+//   NOTE: the last character in the value must be '/'
274
+// - userAuth
275
+//   - userName
276
+//     Set the username you want to login.
277
+//   - userPassword
278
+//     Set the password for the given username.
279
+// NOTE: for description of other properties see comments at the beginning
280
+// of this file.
281
+// NOTE: for minimal/fast setup you need to set only the href and userAuth
282
+// options. It is safe/recommended to keep the remaining options unchanged!
283
+// Example:
284
+//var globalAccountSettings=[
285
+//	{
286
+//		href: 'https://server1.com:8443/caldav.php/USERNAME1/',
287
+//		userAuth:
288
+//		{
289
+//			userName: 'USERNAME1',
290
+//			userPassword: 'PASSWORD1'
291
+//		},
292
+//		timeOut: 90000,
293
+//		lockTimeOut: 10000,
294
+//		checkContentType: true,
295
+//		settingsAccount: true,
296
+//		delegation: true,
297
+//		hrefLabel: null,
298
+//		forceReadOnly: null,
299
+//		ignoreAlarms: false,
300
+//		backgroundCalendars: []
301
+//	},
302
+//	{
303
+//		href: 'https://server2.com:8443/caldav.php/USERNAME2/',
304
+//		...
305
+//		...
306
+//	}
307
+//];
308
+
309
+var globalAccountSettings=[
310
+	{
311
+		href: 'http://127.0.0.1:5232/user/3.ics/',
312
+		userAuth:
313
+		{
314
+			userName: 'bastien.sevajol@algoo.fr',
315
+			userPassword: 'bastien.sevajol@algoo.fr'
316
+			//userName: 'bastien',
317
+			//userPassword: 'bastien'
318
+		},
319
+		timeOut: 90000,
320
+		lockTimeOut: 10000,
321
+		checkContentType: true,
322
+		settingsAccount: true,
323
+		delegation: true,
324
+		hrefLabel: null,
325
+		forceReadOnly: null,
326
+		ignoreAlarms: false,
327
+		backgroundCalendars: []
328
+	}
329
+];
330
+
331
+// globalNetworkCheckSettings
332
+// Use this option if you want to use standard login screen without
333
+// hardcoded username/password in config.js (used by globalAccountSettings).
334
+// NOTE: if this option is used the value must be an object.
335
+// List of properties used in globalAccountSettings variable:
336
+// - href
337
+//   Set this option to the "principal URL" WITHOUT the "USERNAME/"
338
+//   part (this options uses the username from the login screen).
339
+//   NOTE: the last character in the value must be '/'
340
+// NOTE: for description of other properties see comments at the beginning
341
+// of this file.
342
+// NOTE: for minimal/fast setup you need to set only the href option. It is
343
+// safe/recommended to keep the remaining options unchanged!
344
+// Example href values:
345
+// OS X server http example (see misc/readme_osx.txt for server setup):
346
+//   href: 'http://osx.server.com:8008/principals/users/'
347
+// OS X server https example (see misc/readme_osx.txt for server setup):
348
+//   href: 'https://osx.server.com:8443/principals/users/'
349
+// Cyrus server https example:
350
+//   href: 'https://cyrus.server.com/dav/principals/user/'
351
+// Example:
352
+// Davical example which automatically detects the protocol, server name,
353
+// port, ... (client installed into Davical "htdocs" subdirectory;
354
+// works "out of the box", no additional setup required):
355
+// var globalNetworkCheckSettings={
356
+// 	href: location.protocol+'//'+location.hostname+
357
+// 		(location.port ? ':'+location.port: '')+
358
+// 		location.pathname.replace(RegExp('/+[^/]+/*(index\.html)?$'),'')+
359
+// 		'/caldav.php/',
360
+// 	timeOut: 90000,
361
+// 	lockTimeOut: 10000,
362
+//      checkContentType: true,
363
+// 	settingsAccount: true,
364
+//      delegation: true,
365
+// 	additionalResources: [],
366
+//      hrefLabel: null,
367
+// 	forceReadOnly: null,
368
+//      ignoreAlarms: false,
369
+// 	backgroundCalendars: []
370
+// }
371
+
372
+
373
+// globalNetworkAccountSettings
374
+// Try this option ONLY if you have working setup using
375
+// globalNetworkCheckSettings and want to fix the authentication popup
376
+// window problem (if invalid username/password is entered)!
377
+// If you use this option then your browser sends username/password to the PHP
378
+// "auth" module ("auth" directory) instead of the DAV server itself.
379
+// The "auth" module then validates your username/password against your server,
380
+// and if the authentication is successful, then it sends back a configuration
381
+// XML (requires additional configuration). The resulting XML is handled
382
+// IDENTICALLY as the globalAccountSettings configuration option.
383
+// NOTE: for the "auth" module configuration see readme.txt!
384
+// NOTE: this option invokes a login screen and disallows access until
385
+// the client gets correct XML configuration file from the server!
386
+// List of properties used in globalNetworkAccountSettings variable:
387
+// - href
388
+//   Set this option to the "full URL" of the "auth" directory
389
+//   NOTE: the last character in the value must be '/'
390
+// NOTE: for description of other properties see comments at the beginning
391
+// of this file.
392
+// Example href values:
393
+//   href: 'https://server.com/client/auth/'
394
+// Example:
395
+// Use this configuration if the "auth" module is located in the client
396
+// installation subdirectory (default):
397
+//var globalNetworkAccountSettings={
398
+//	href: location.protocol+'//'+location.hostname+
399
+//		(location.port ? ':'+location.port : '')+
400
+//		location.pathname.replace(RegExp('index\.html$'),'')+
401
+//		'auth/',
402
+//	timeOut: 30000
403
+//};
404
+
405
+
406
+// globalUseJqueryAuth
407
+// Use jQuery .ajax() auth or custom header for HTTP basic auth (default).
408
+// Set this option to true if your server uses digest auth (note: you may
409
+// experience auth popups on some browsers).
410
+// If undefined (or empty), custom header for HTTP basic auth is used.
411
+// Example:
412
+//var globalUseJqueryAuth=false;
413
+
414
+
415
+// globalBackgroundSync
416
+// Enable background synchronization even if the browser window/tab has no
417
+// focus.
418
+// If false, synchronization is performed only if the browser window/tab
419
+// is focused. If undefined or not false, then background sync is enabled.
420
+// Example:
421
+var globalBackgroundSync=true;
422
+
423
+
424
+// globalSyncResourcesInterval
425
+// This option defines how often (in miliseconds) are resources/collections
426
+// asynchronously synchronized.
427
+// Example:
428
+var globalSyncResourcesInterval=120000;
429
+
430
+
431
+// globalEnableRefresh
432
+// This option enables or disables the manual synchronization button in
433
+// the interface. If this option is enabled then users can perform server
434
+// synchronization manually. Enabling this option may cause high server
435
+// load (even DDOS) if users will try to manually synchronize data too
436
+// often (instead of waiting for the automatic synchronization).
437
+// If undefined or false, the synchronization button is disabled.
438
+// NOTE: enable this option only if you really know what are you doing!
439
+// Example:
440
+var globalEnableRefresh=false;
441
+
442
+
443
+// globalEnableKbNavigation
444
+// Enable basic keyboard navigation using arrow keys?
445
+// If undefined or not false, keyboard navigation is enabled.
446
+// Example:
447
+var globalEnableKbNavigation=true;
448
+
449
+
450
+// globalSettingsType
451
+// Where to store user settings such as: active view, enabled/selected
452
+// collections, ... (the client store them into DAV property on the server).
453
+// NOTE: not all servers support storing DAV properties (some servers support
454
+// only subset /or none/ of these URLs).
455
+// Supported values:
456
+// - 'principal-URL', '', null or undefined (default) => settings are stored
457
+//   to principal-URL (recommended for most servers)
458
+// - 'addressbook-home-set' => settings are are stored to addressbook-home-set
459
+// Example:
460
+//var globalSettingsType='';
461
+
462
+
463
+// globalCrossServerSettingsURL
464
+// Settings such as enabled/selected collections are stored on the server
465
+// (see the previous option) in form of full URL
466
+// (e.g.: https://user@server:port/principal/collection/), but even if this
467
+// approach is "correct" (you can use the same principal URL with multiple
468
+// different logins, ...) it causes a problem if your server is accessible
469
+// from multiple URLs (e.g. http://server/ and https://server/). If you want
470
+// to store only the "principal/collection/" part of the URL (instead of the
471
+// full URL) then enable this option.
472
+// Example:
473
+//var globalCrossServerSettingsURL=false;
474
+
475
+
476
+// globalInterfaceLanguage
477
+// Default interface language (note: this option is case sensitive):
478
+//   cs_CZ (Čeština [Czech])
479
+//   da_DK (Dansk [Danish]; thanks Niels Bo Andersen)
480
+//   de_DE (Deutsch [German]; thanks Marten Gajda and Thomas Scheel)
481
+//   en_US (English [English/US])
482
+//   es_ES (Español [Spanish]; thanks Damián Vila)
483
+//   fr_FR (Français [French]; thanks John Fischer)
484
+//   it_IT (Italiano [Italian]; thanks Luca Ferrario)
485
+//   ja_JP (日本語 [Japan]; thanks Muimu Nakayama)
486
+//   hu_HU (Magyar [Hungarian])
487
+//   nl_NL (Nederlands [Dutch]; thanks Johan Vromans)
488
+//   sk_SK (Slovenčina [Slovak])
489
+//   tr_TR (Türkçe [Turkish]; thanks Selcuk Pultar)
490
+//   ru_RU (Русский [Russian]; thanks Александр Симонов)
491
+//   uk_UA (Українська [Ukrainian]; thanks Serge Yakimchuck)
492
+//   zh_CN (中国 [Chinese]; thanks Fandy)
493
+// Example:
494
+var globalInterfaceLanguage='en_US';
495
+
496
+
497
+// globalInterfaceCustomLanguages
498
+// If defined and not empty then only languages listed here are shown
499
+// at the login screen, otherwise (default) all languages are shown
500
+// NOTE: values in the array must refer to an existing localization
501
+// (see the option above)
502
+// Example:
503
+//   globalInterfaceCustomLanguages=['en_US', 'sk_SK'];
504
+var globalInterfaceCustomLanguages=[];
505
+
506
+
507
+// globalSortAlphabet
508
+// Use JavaScript localeCompare() or custom alphabet for data sorting.
509
+// Custom alphabet is used by default because JavaScript localeCompare()
510
+// not supports collation and often returns "wrong" result. If set to null
511
+// then localeCompare() is used.
512
+// Example:
513
+//   var globalSortAlphabet=null;
514
+var globalSortAlphabet=' 0123456789'+
515
+	'AÀÁÂÄÆÃÅĀBCÇĆČDĎEÈÉÊËĒĖĘĚFGĞHIÌÍÎİÏĪĮJKLŁĹĽMNŃÑŇOÒÓÔÖŐŒØÕŌ'+
516
+	'PQRŔŘSŚŠȘșŞşẞTŤȚțŢţUÙÚÛÜŰŮŪVWXYÝŸZŹŻŽ'+
517
+	'aàáâäæãåābcçćčdďeèéêëēėęěfgğhiìíîïīįıjklłĺľmnńñňoòóôöőœøõō'+
518
+	'pqrŕřsśšßtťuùúûüűůūvwxyýÿzźżžАБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЮЯ'+
519
+	'Ьабвгґдеєжзиіїйклмнопрстуфхцчшщюяь';
520
+
521
+
522
+// globalSearchTransformAlphabet
523
+// To support search without diacritics (e.g. search for 'd' will find: 'Ď', 'ď')
524
+// it is required to define something like "character equivalence".
525
+// key = regex text, value = search character
526
+// Example:
527
+var globalSearchTransformAlphabet={
528
+	'[ÀàÁáÂâÄäÆæÃãÅåĀā]': 'a', '[ÇçĆćČč]': 'c', '[Ďď]': 'd',
529
+	'[ÈèÉéÊêËëĒēĖėĘęĚě]': 'e', '[Ğğ]': 'g', '[ÌìÍíÎîİıÏïĪīĮį]': 'i',
530
+	'[ŁłĹ弾]': 'l', '[ŃńÑñŇň]': 'n', '[ÒòÓóÔôÖöŐőŒœØøÕõŌō]': 'o',
531
+	'[ŔŕŘř]': 'r', '[ŚśŠšȘșŞşẞß]': 's', '[ŤťȚțŢţ]': 't',
532
+	'[ÙùÚúÛûÜüŰűŮůŪū]': 'u', '[ÝýŸÿ]': 'y', '[ŹźŻżŽž]': 'z'
533
+};
534
+
535
+// globalResourceAlphabetSorting
536
+// If more than one resource (server account) is configured, sort the
537
+// resources alphabetically?
538
+// Example:
539
+var globalResourceAlphabetSorting=true;
540
+
541
+
542
+// globalNewVersionNotifyUsers
543
+// Update notification will be shown only to users with login names defined
544
+// in this array.
545
+// If undefined (or empty), update notifications will be shown to all users.
546
+// Example:
547
+//   globalNewVersionNotifyUsers=['admin', 'peter'];
548
+var globalNewVersionNotifyUsers=[];
549
+
550
+
551
+// globalDatepickerFormat
552
+// Set the datepicker format (see 
553
+// http://docs.jquery.com/UI/Datepicker/formatDate for valid values).
554
+// NOTE: date format is predefined for each localization - use this option
555
+// ONLY if you want to use custom date format (instead of the localization
556
+// predefined one).
557
+// Example:
558
+//var globalDatepickerFormat='dd.mm.yy';
559
+
560
+
561
+// globalDatepickerFirstDayOfWeek
562
+// Set the datepicker first day of the week: Sunday is 0, Monday is 1, etc.
563
+// Example:
564
+var globalDatepickerFirstDayOfWeek=1;
565
+
566
+
567
+// globalHideInfoMessageAfter
568
+// How long are information messages (such as: success, error) displayed
569
+// (in miliseconds).
570
+// Example:
571
+var globalHideInfoMessageAfter=1800;
572
+
573
+
574
+// globalEditorFadeAnimation
575
+// Set the editor fade in/out animation duration when editing or saving data
576
+// (in miliseconds).
577
+// Example:
578
+var globalEditorFadeAnimation=666;
579
+
580
+
581
+
582
+// globalEventStartPastLimit, globalEventStartFutureLimit, globalTodoPastLimit
583
+// Number of months pre-loaded from past/future in advance for calendars
584
+// and todo lists (if null then date range synchronization is disabled).
585
+// NOTE: interval synchronization is used only if your server supports
586
+// sync-collection REPORT (e.g. DAViCal).
587
+// NOTE: if you experience problems with data loading and your server has
588
+// no time-range filtering support set these variables to null.
589
+// Example:
590
+var globalEventStartPastLimit=3;
591
+var globalEventStartFutureLimit=3;
592
+var globalTodoPastLimit=1;
593
+
594
+
595
+// globalLoadedCalendarCollections
596
+// This option sets the list of calendar collections (down)loaded after login.
597
+// If empty then all calendar collections for the currently logged user are
598
+// loaded.
599
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
600
+// option.
601
+// Example:
602
+var globalLoadedCalendarCollections=[];
603
+
604
+
605
+// globalLoadedTodoCollections
606
+// This option sets the list of todo collections (down)loaded after login.
607
+// If empty then all todo collections for the currently logged user are loaded.
608
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
609
+// option.
610
+// Example:
611
+var globalLoadedTodoCollections=[];
612
+
613
+
614
+// globalActiveCalendarCollections
615
+// This options sets the list of calendar collections checked (enabled
616
+// checkbox => data visible in the interface) by default after login.
617
+// If empty then all loaded calendar collections for the currently logged
618
+// user are checked.
619
+// NOTE: only already (down)loaded collections can be checked (see 
620
+// the globalLoadedCalendarCollections option).
621
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
622
+// option.
623
+// Example:
624
+var globalActiveCalendarCollections=[];
625
+
626
+
627
+// globalActiveTodoCollections
628
+// This options sets the list of todo collections checked (enabled
629
+// checkbox => data visible in the interface) by default after login.
630
+// If empty then all loaded todo collections for the currently logged
631
+// user are checked.
632
+// NOTE: only already (down)loaded collections can be checked (see 
633
+// the globalLoadedTodoCollections option).
634
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
635
+// option.
636
+// Example:
637
+var globalActiveTodoCollections=[];
638
+
639
+
640
+// globalCalendarSelected
641
+// This option sets which calendar collection will be pre-selected
642
+// (if you create a new event) by default after login.
643
+// The value must be URL encoded path to a calendar collection,
644
+// for example: 'USER/calendar/'
645
+// If empty or undefined then the first available calendar collection
646
+// is selected automatically.
647
+// NOTE: only already (down)loaded collections can be pre-selected (see
648
+// the globalLoadedCalendarCollections option).
649
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
650
+// option.
651
+// Example:
652
+//var globalCalendarSelected='';
653
+
654
+
655
+// globalTodoCalendarSelected
656
+// This option sets which todo collection will be pre-selected
657
+// (if you create a new todo) by default after login.
658
+// The value must be URL encoded path to a todo collection,
659
+// for example: 'USER/todo_calendar/'
660
+// If empty or undefined then the first available todo collection
661
+// is selected automatically.
662
+// NOTE: only already (down)loaded collections can be pre-selected (see 
663
+// the globalLoadedTodoCollections option).
664
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
665
+// option.
666
+// Example:
667
+//var globalTodoCalendarSelected='';
668
+
669
+
670
+// globalActiveView
671
+// This options sets the default fullcalendar view option (the default calendar
672
+// view after the first login).
673
+// Supported values:
674
+// - 'month'
675
+// - 'multiWeek'
676
+// - 'agendaWeek'
677
+// - 'agendaDay'
678
+// NOTE: we use custom and enhanced version of fullcalendar!
679
+// Example:
680
+var globalActiveView='multiWeek';
681
+
682
+
683
+// globalOpenFormMode
684
+// Open new event form on 'single' or 'double' click.
685
+// If undefined or not 'double', then 'single' is used.
686
+// Example:
687
+var globalOpenFormMode='double';
688
+
689
+
690
+// globalTodoListFilterSelected
691
+// This options sets the list of filters in todo list that are selected
692
+// after login.
693
+// Supported options:
694
+// - 'filterAction'
695
+// - 'filterProgress' (available only if globalAppleRemindersMode is disabled)
696
+// - 'filterCompleted'
697
+// - 'filterCanceled' (available only if globalAppleRemindersMode is disabled)
698
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
699
+// option.
700
+// Example:
701
+var globalTodoListFilterSelected=['filterAction', 'filterProgress'];
702
+
703
+
704
+// globalCalendarStartOfBusiness, globalCalendarEndOfBusiness
705
+// These options set the start and end of business hours with 0.5 hour
706
+// precision. Non-business hours are faded out in the calendar interface.
707
+// If both variables are set to the same value then no fade out occurs.
708
+// Example:
709
+var globalCalendarStartOfBusiness=8;
710
+var globalCalendarEndOfBusiness=17;
711
+
712
+
713
+// globalDefaultEventDuration
714
+// This option sets the default duration (in minutes) for newly created events.
715
+// If undefined or null, globalCalendarEndOfBusiness value will be taken as
716
+// a default end time instead.
717
+// Example:
718
+var globalDefaultEventDuration=120;
719
+
720
+
721
+// globalAMPMFormat
722
+// This option enables to use 12 hours format (AM/PM) for displaying time.
723
+// NOTE: time format is predefined for each localization - use this option
724
+// ONLY if you want to use custom time format (instead of the localization
725
+// predefined one).
726
+// Example:
727
+//var globalAMPMFormat=false;
728
+
729
+
730
+// globalTimeFormatBasic
731
+// This option defines the time format information for events in month and
732
+// multiweek views. If undefined or null then default value is used.
733
+// If defined as empty string no time information is shown in these views.
734
+// See http://arshaw.com/fullcalendar/docs/utilities/formatDate/ for exact
735
+// formating rules.
736
+// Example:
737
+//var globalTimeFormatBasic='';
738
+
739
+
740
+// globalTimeFormatAgenda
741
+// This option defines the time format information for events in day and
742
+// week views. If undefined or null then default value is used.
743
+// If defined as empty string no time information is shown in these views.
744
+// See http://arshaw.com/fullcalendar/docs/utilities/formatDate/ for exact
745
+// formating rules.
746
+// Example:
747
+//var globalTimeFormatAgenda='';
748
+
749
+
750
+// globalDisplayHiddenEvents
751
+// This option defined whether events from unechecked calendars are displayed
752
+// with certain transparency (true) or completely hidden (false).
753
+// Example:
754
+var globalDisplayHiddenEvents=false;
755
+
756
+
757
+// globalTimeZoneSupport
758
+// This option enables timezone support in the client.
759
+// NOTE: timezone cannot be specified for all-day events because these don't
760
+// have start and end time.
761
+// If this option is disabled then local time is used.
762
+// Example:
763
+var globalTimeZoneSupport=true;
764
+
765
+
766
+// globalTimeZone
767
+// If timezone support is enabled, this option sets the default timezone.
768
+// See timezones.js or use the following command to get the list of supported
769
+// timezones (defined in timezones.js):
770
+// grep "'[^']\+': {" timezones.js | sed -Ee "s#(\s*'|':\s*\{)##g"
771
+// Example:
772
+var globalTimeZone='Europe/Berlin';
773
+
774
+
775
+// globalTimeZonesEnabled
776
+// This option sets the list of available timezones in the interface (for the 
777
+// list of supported timezones see the comment for the previous configuration
778
+// option).
779
+// NOTE: if there is at least one event/todo with a certain timezone defined,
780
+// that timezone is enabled (even if it is not present in this list).
781
+// Example:
782
+//   var globalTimeZonesEnabled=['America/New_York', 'Europe/Berlin'];	
783
+var globalTimeZonesEnabled=[];
784
+
785
+
786
+// globalRewriteTimezoneComponent
787
+// This options sets whether the client will enhance/replace (if you edit an
788
+// event or todo) the timezone information using the official IANA timezone
789
+// database information (recommended).
790
+// Example:
791
+var globalRewriteTimezoneComponent=true;
792
+
793
+
794
+// globalRemoveUnknownTimezone
795
+// This options sets whether the client will remove all non-standard timezone
796
+// names from events and todos (if you edit an event or todo)
797
+// (e.g.: /freeassociation.sourceforge.net/Tzfile/Europe/Vienna)
798
+// Example:
799
+var globalRemoveUnknownTimezone=false;
800
+
801
+
802
+// globalShowHiddenAlarms
803
+// This option sets whether the client will show alarm notifications for
804
+// unchecked calendars. If this option is enabled and you uncheck a calendar
805
+// in the calendar list, alarm notifications will be temporary disabled for
806
+// unchecked calendar(s).
807
+// Example:
808
+var globalShowHiddenAlarms=false;
809
+
810
+
811
+// globalIgnoreCompletedOrCancelledAlarms
812
+// This options sets whether the client will show alarm notifications for
813
+// already completed or cancelled todos. If enabled then alarm notification
814
+// for completed and cancelled todos are disabled.
815
+// Example:
816
+var globalIgnoreCompletedOrCancelledAlarms=true;
817
+
818
+
819
+// globalMozillaSupport
820
+// Mozilla automatically treats custom repeating event calculations as if
821
+// the start day of the week is Monday, despite what day is chosen in settings.
822
+// Set this variable to true to use the same approach, ensuring compatible
823
+// event rendering in special cases.
824
+// Example:
825
+var globalMozillaSupport=false;
826
+
827
+
828
+// globalCalendarColorPropertyXmlns
829
+// This options sets the namespace used for storing the "calendar-color"
830
+// property by the client.
831
+// If true, undefined (or empty) "http://apple.com/ns/ical/" is used (Apple
832
+// compatible). If false, then the calendar color modification functionality
833
+// is completely disabled.
834
+// Example:
835
+//var globalCalendarColorPropertyXmlns=true;
836
+
837
+
838
+// globalWeekendDays
839
+// This option sets the list of days considered as weekend days (these
840
+// are faded out in the calendar interface). Non-weekend days are automatically
841
+// considered as business days.
842
+// Sunday is 0, Monday is 1, etc.
843
+// Example:
844
+var globalWeekendDays=[0, 6];
845
+
846
+
847
+// globalAppleRemindersMode
848
+// If this option is enabled then then client will use the same approach
849
+// for handling repeating reminders (todos) as Apple. It is STRONGLY
850
+// recommended to enabled this option if you use any Apple clients for
851
+// reminders (todos).
852
+// Supported options:
853
+// - 'iOS6'
854
+// - 'iOS7'
855
+// - true (support of the latest iOS version - 'iOS8')
856
+// - false
857
+// If this option is enabled:
858
+// - RFC todo support is SEVERELY limited and the client mimics the behaviour
859
+//   of Apple Reminders.app (to ensure maximum compatibility)
860
+// - when a single instance of repeating todo is edited, it becomes an
861
+//   autonomous non-repeating todo with NO relation to the original repeating
862
+//   todo
863
+// - capabilities of repeating todos are limited - only the first instance
864
+//   is ever visible in the interface
865
+// - support for todo DTSTART attribute is disabled
866
+// - support for todo STATUS attribute other than COMPLETED and NEEDS-ACTION
867
+//   is disabled
868
+// - [iOS6 only] support for LOCATION and URL attributes is disabled
869
+// Example:
870
+var globalAppleRemindersMode=true;
871
+
872
+
873
+// globalSubscribedCalendars
874
+// This option specifies a list of remote URLs to ics files (e.g.: used
875
+// for distributing holidays information). Subscribed calendars are
876
+// ALWAYS read-only. Remote servers where ics files are hosted MUST
877
+// return proper CORS headers (see readme.txt) otherwise this functionality
878
+// will not work!
879
+// NOTE: subsribed calendars are NOT "shared" calendars. For "shared"
880
+// calendars see the delegation option in globalAccountSettings,
881
+// globalNetworkCheckSettings and globalNetworkAccountSettings.
882
+// List of properties used in globalSubscribedCalendars variable:
883
+// - hrefLabel
884
+//   This options defines the header string above the subcsribed calendars.
885
+// - calendars
886
+//   This option specifies an array of remote calendar objects with the
887
+//   following properties:
888
+//   - href
889
+//     Set this option to the "full URL" of the remote calendar
890
+//   - userAuth
891
+//     NOTE: keep empty if remote authentication is not required!
892
+//     - userName
893
+//       Set the username you want to login.
894
+//     - userPassword
895
+//       Set the password for the given username.
896
+//   - typeList
897
+//     Set the list of objects you want to process from remote calendars;
898
+//     two options are available:
899
+//     - 'vevent' (show remote events in the interface) 
900
+//     - 'vtodo' (show remote todos in the interface) 
901
+//   - ignoreAlarm
902
+//     Set this option to true if you want to disable alarm notifications
903
+//     from the remote calendar.
904
+//   - displayName
905
+//     Set this option to the name of the calendar you want to see
906
+//     in the interface.
907
+//   - color
908
+//     Set the calendar color you want to see in the interface.
909
+// Example:
910
+//var globalSubscribedCalendars={
911
+//	hrefLabel: 'Subscribed',
912
+//	calendars: [
913
+//		{
914
+//			href: 'http://something.com/calendar.ics',
915
+//			userAuth: {
916
+//				userName: '',
917
+//				userPassword: ''
918
+//			},
919
+//			typeList: ['vevent', 'vtodo'],
920
+//			ignoreAlarm: true,
921
+//			displayName: 'Remote Calendar 1',
922
+//			color: '#ff0000'
923
+//		},
924
+//		{
925
+//			href: 'http://calendar.com/calendar2.ics',
926
+//			...
927
+//			...
928
+//		}
929
+//	]
930
+//};
931
+

文件差異過大導致無法顯示
+ 2715 - 0
tracim/tracim/public/caldavzap/css/default.css


+ 180 - 0
tracim/tracim/public/caldavzap/css/default_integration.css 查看文件

@@ -0,0 +1,180 @@
1
+.integration_d
2
+{
3
+	position: absolute;
4
+	display: none;
5
+	overflow: hidden;
6
+	top: 0;
7
+	bottom: 0;
8
+	left: 0;
9
+	width: 49px;
10
+	background: #f0f0f0;
11
+	color: #FFFFFF;
12
+	border-right: 1px solid #c0c0c0;
13
+	z-index: 26;
14
+	padding-top: 3px;
15
+	cursor: default;
16
+
17
+	user-select: none;
18
+	-webkit-user-select: none;
19
+	-moz-user-select: none;
20
+}
21
+
22
+.integration_d div
23
+{
24
+	display: none;
25
+	height: 36px;
26
+	width: 36px;
27
+	padding: 7px 7px;
28
+	cursor: pointer;
29
+}
30
+
31
+.integration_d .intBlank
32
+{
33
+	cursor: default;
34
+}
35
+
36
+#intCaldav
37
+{
38
+	background: url(../images/banner_calendar.svg) no-repeat center;
39
+}
40
+
41
+#intCaldavTodo
42
+{
43
+	background: url(../images/banner_todo.svg) no-repeat center;
44
+}
45
+
46
+#intCarddav
47
+{
48
+	background: url(../images/banner_addressbook.svg) no-repeat center;
49
+}
50
+
51
+#intProjects
52
+{
53
+	background: url(../images/banner_projects.svg) no-repeat center;
54
+}
55
+
56
+#intReports
57
+{
58
+	background: url(../images/banner_reports.svg) no-repeat center;
59
+}
60
+
61
+#intSettings
62
+{
63
+	background: url(../images/banner_settings.svg) no-repeat center;
64
+}
65
+
66
+#intRefresh
67
+{
68
+	background: url(../images/banner_refresh.svg) no-repeat center;
69
+}
70
+
71
+#intLogout
72
+{
73
+	background: url(../images/banner_logout.svg) no-repeat center;
74
+}
75
+
76
+.int_error
77
+{
78
+	display: none;
79
+	margin: 28px 0px 0px 28px;
80
+}
81
+
82
+#resourceCalDAV_h, #resourceCalDAVTODO_h, #ResourceCalDAVList, #ResourceCalDAVTODOList, #timezoneWrapper, #timezoneWrapperTODO
83
+{
84
+	left: 50px;
85
+}
86
+
87
+#main, #main_h, #searchForm, #mainTODO, #main_h_TODO, #searchFormTODO
88
+{
89
+	left: 274px;
90
+}
91
+
92
+#MainLoader, #EventDisabler, #TodoDisabler, #AlertDisabler, #ProjectsDisabler
93
+{
94
+	left: 50px;
95
+}
96
+
97
+#CalendarLoader, #CalendarLoaderTODO
98
+{
99
+	left: 275px;
100
+}
101
+
102
+.resourcesCardDAV_d, #ResourceCardDAVList, #ResourceCardDAVListOverlay
103
+{
104
+	left: 50px;
105
+}
106
+
107
+.collection_d, #SearchBox, #ABList, #ABListOverlay, #AddressbookOverlay
108
+{
109
+	left: 275px;
110
+}
111
+
112
+/*.contact_d, #ABContactColor, #ABContactOverlay, #ABMessage
113
+{
114
+	left: 526px;
115
+}
116
+
117
+#ABContact
118
+{
119
+	left: 529px;
120
+}*/
121
+
122
+.filters_d, .statistics_d, #FilterList, #ProjectsData, #SystemProjectsLock, #SystemReportsLock, #ProjectListDisabler
123
+{
124
+	left: 50px;
125
+}
126
+
127
+.projects_d, #ProjectList, #SearchBoxProject, #ProjectListOverlay
128
+{
129
+	left: 375px;
130
+}
131
+
132
+.project_d, #ProjectForm, #ProjectFormLoader, #ProjectEventsContainer, #ActivityListDisabler, #ProjectFormMessage
133
+{
134
+	left: 626px;
135
+}
136
+
137
+.resourcesReports_d, #ResourceReportsList
138
+{
139
+	left: 50px;
140
+}
141
+
142
+.resourcesSettings_d, #ResourceSettingsList
143
+{
144
+	left: 50px;
145
+}
146
+
147
+#ResourceReportsListOverlay
148
+{
149
+	left: 50px;
150
+}
151
+
152
+.report_filters_d, #ReportFilterList
153
+{
154
+	left: 275px;
155
+}
156
+
157
+.reports_d, #ReportsColor, #ReportsFormOverlay
158
+{
159
+	left: 600px;
160
+}
161
+
162
+#ReportsForm
163
+{
164
+	left: 603px;
165
+}
166
+
167
+#ResourceSettingsListOverlay
168
+{
169
+	left: 50px;
170
+}
171
+
172
+.settings_d, #SettingsColor, #SettingsFormOverlay
173
+{
174
+	left: 275px;
175
+}
176
+
177
+#SettingsForm
178
+{
179
+	left: 278px;
180
+}

文件差異過大導致無法顯示
+ 1464 - 0
tracim/tracim/public/caldavzap/css/fullcalendar.css


+ 203 - 0
tracim/tracim/public/caldavzap/css/jquery-ui.custom.css 查看文件

@@ -0,0 +1,203 @@
1
+/*
2
+ * jQuery UI CSS Framework
3
+ *
4
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
5
+ * Dual licensed under the MIT or GPL Version 2 licenses.
6
+ * http://jquery.org/license
7
+ *
8
+ * http://docs.jquery.com/UI/Theming/API
9
+ */
10
+
11
+/* Layout helpers
12
+----------------------------------*/
13
+.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; }
14
+.ui-helper-clearfix:after { clear: both; }
15
+.ui-helper-clearfix { zoom: 1; }
16
+.ui-helper-hidden-accessible { display: none; }
17
+
18
+/* Interaction Cues
19
+----------------------------------*/
20
+.ui-state-disabled { cursor: default !important; }
21
+
22
+/* states and images */
23
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
24
+
25
+/*
26
+ * jQuery UI CSS Framework
27
+ *
28
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
29
+ * Dual licensed under the MIT or GPL Version 2 licenses.
30
+ * http://jquery.org/license
31
+ *
32
+ * http://docs.jquery.com/UI/Theming/API
33
+ *
34
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS,%20Tahoma,%20Verdana,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=12_gloss_wave.png&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=03_highlight_soft.png&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=08_diagonals_thick.png&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=08_diagonals_thick.png&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px
35
+ */
36
+
37
+/* Component containers
38
+----------------------------------*/
39
+.ui-widget { font-family: inherit; font-size: 1em; }
40
+.ui-widget-content { border: 1px solid #c0c0c0; background: #ffffff; color: #404040; }
41
+.ui-widget-header { border: none; background: #f0f0f0; color: #404040; font-weight: 500; }
42
+
43
+/* Interaction states
44
+----------------------------------*/
45
+.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #c0c0c0; background: #ffffff; font-weight: 400; color: #404040; }
46
+.ui-widget-content .ui-datepicker-week-end .ui-state-default {background: #f7f7f7;}
47
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555; text-decoration: none; }
48
+.ui-state-hover, .ui-widget-content .ui-datepicker-week-end .ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { background: #e7e7e7; font-weight: 400; color: #404040; }
49
+.ui-state-hover a, .ui-state-hover a:hover, .ui-widget-content .ui-datepicker-week-end .ui-state-hover a:hover { color: #212121; text-decoration: none;}
50
+.ui-widget-content .ui-datepicker-today .ui-state-default {background: #c0c0c0; color: #404040;}
51
+.ui-state-active, .ui-widget-content .ui-datepicker-week-end .ui-state-active, .ui-widget-content .ui-datepicker-today .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #585858; background: #585858; font-weight: 400; color: #ffffff; }
52
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121; text-decoration: none; }
53
+.ui-widget :active { outline: none; }
54
+
55
+/* Interaction Cues
56
+----------------------------------*/
57
+.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #fef1ec; color: #cd0a0a; }
58
+.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a; }
59
+.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a; }
60
+.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: 500; }
61
+
62
+/* states and images */
63
+.ui-icon { width: 17px; height: 19px; }
64
+
65
+/* positioning */
66
+.ui-icon-circle-triangle-e { background-image: url(../images/dp_right.svg); }
67
+.ui-icon-circle-triangle-w { background-image: url(../images/dp_left.svg); }
68
+
69
+/*
70
+ * jQuery UI Autocomplete
71
+ *
72
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
73
+ * Dual licensed under the MIT or GPL Version 2 licenses.
74
+ * http://jquery.org/license
75
+ *
76
+ * http://docs.jquery.com/UI/Autocomplete#theming
77
+ */
78
+.ui-autocomplete { position: absolute; cursor: default; max-height: 80px; overflow-y: auto; overflow-x: hidden; padding-right: 20px;}
79
+
80
+/* workarounds */
81
+* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
82
+
83
+/*
84
+ * jQuery UI Menu
85
+ *
86
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
87
+ * Dual licensed under the MIT or GPL Version 2 licenses.
88
+ * http://jquery.org/license
89
+ *
90
+ * http://docs.jquery.com/UI/Menu#theming
91
+ */
92
+.ui-menu {
93
+	list-style: none;
94
+	padding: 0px;
95
+	margin: 0;
96
+	display: block;
97
+	float: left;
98
+	border-color: #e0e0e0;
99
+}
100
+.ui-menu .ui-menu {
101
+	margin-top: -3px;
102
+}
103
+.ui-menu .ui-menu-item {
104
+	margin: 0;
105
+	padding: 0;
106
+	zoom: 1;
107
+	float: left;
108
+	clear: left;
109
+	width: 100%;
110
+	white-space: pre;
111
+}
112
+.ui-menu .ui-menu-item a {
113
+	text-decoration: none;
114
+	display: block;
115
+	padding: 2px 2px;
116
+	zoom: 1;
117
+	white-space: pre;
118
+}
119
+.ui-menu .ui-menu-item a.ui-state-hover,
120
+.ui-menu .ui-menu-item a.ui-state-active {
121
+	font-weight: 400;
122
+	background: #f0f0f0;
123
+}
124
+
125
+button.ui-datepicker-current
126
+{
127
+  width:40%;
128
+  margin:5px 1px 5px 1px;
129
+  float:left;
130
+}
131
+button.ui-datepicker-close
132
+{
133
+  width:40%;
134
+  margin:5px 1px 5px 1px;
135
+  float:right;
136
+}
137
+
138
+/*
139
+ * jQuery UI Slider
140
+ *
141
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
142
+ * Dual licensed under the MIT or GPL Version 2 licenses.
143
+ * http://jquery.org/license
144
+ *
145
+ * http://docs.jquery.com/UI/Slider#theming
146
+ */
147
+.ui-slider { position: relative; text-align: left; }
148
+.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
149
+.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
150
+
151
+.ui-slider-horizontal { height: .8em; }
152
+.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
153
+.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
154
+.ui-slider-horizontal .ui-slider-range-min { left: 0; }
155
+.ui-slider-horizontal .ui-slider-range-max { right: 0; }
156
+
157
+.ui-slider-vertical { width: .8em; height: 100px; }
158
+.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
159
+.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
160
+.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
161
+.ui-slider-vertical .ui-slider-range-max { top: 0; }
162
+
163
+/*
164
+ * jQuery UI Datepicker
165
+ *
166
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
167
+ * Dual licensed under the MIT or GPL Version 2 licenses.
168
+ * http://jquery.org/license
169
+ *
170
+ * http://docs.jquery.com/UI/Datepicker#theming
171
+ */
172
+
173
+.ui-datepicker { width: 184px; padding: 4px; z-index: 21; display: none; cursor:default; user-select: none; -webkit-user-select:none; -moz-user-select: -moz-none;}
174
+.ui-datepicker .ui-datepicker-header { position:relative; height: 19px; padding: 2px 0px; margin: 2px 2px 1px 2px; }
175
+.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 17px; height: 19px; }
176
+.ui-datepicker .ui-datepicker-prev { left: 2px; }
177
+.ui-datepicker .ui-datepicker-next { right: 2px; }
178
+.ui-datepicker .ui-datepicker-title { margin: 0px 24px; text-align: center; white-space: nowrap;}
179
+.ui-datepicker .ui-datepicker-title select { margin: 0px 0px; background-color: #ffffff; }
180
+.ui-datepicker .ui-datepicker-title span { display: inline-block; margin-top: 3px; }
181
+.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
182
+.ui-datepicker select.ui-datepicker-month { padding-bottom: 0px; width: 40px; max-height: 19px; line-height: 19px; margin-left: 2px; }
183
+.ui-datepicker select.ui-datepicker-year { padding-bottom: 0px; width: 57px; max-height: 19px; line-height: 19px; }
184
+.ui-datepicker table { font-size: 0.95em; border-collapse: collapse; margin: 3px 0px 1px 1px; }
185
+.ui-datepicker th { display: none; }
186
+.ui-datepicker td { border: 0; padding: 2px; }
187
+.ui-datepicker td span, .ui-datepicker td a { display: block; width: 18px; padding: 1px 2px 1px 0px; text-align: right; text-decoration: none; overflow: hidden; }
188
+.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border: 0; }
189
+.ui-datepicker .ui-datepicker-buttonpane button { border: 1px solid #c0c0c0; background: #ffffff; font-weight: 400; color: #000000; padding: 0px;}
190
+.ui-datepicker-calendar th {font-weight: 400;}
191
+
192
+/* RTL support */
193
+.ui-datepicker-rtl { direction: rtl; }
194
+.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
195
+.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
196
+.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
197
+.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
198
+.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
199
+.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
200
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
201
+.ui-datepicker-rtl .ui-datepicker-group { float:right; }
202
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width: 0px; border-left-width:1px; }
203
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width: 0px; border-left-width:1px; }

+ 553 - 0
tracim/tracim/public/caldavzap/css/spectrum.custom.css 查看文件

@@ -0,0 +1,553 @@
1
+/***
2
+Spectrum Colorpicker v1.2.0
3
+https://github.com/bgrins/spectrum
4
+Author: Brian Grinstead
5
+License: MIT
6
+***/
7
+
8
+.sp-container {
9
+	position:absolute;
10
+	top:0;
11
+	left:0;
12
+	display:inline-block;
13
+	*display: inline;
14
+	*zoom: 1;
15
+	/* https://github.com/bgrins/spectrum/issues/40 */
16
+	z-index: 9999994;
17
+	overflow: hidden;
18
+	border-bottom: 1px solid #c0c0c0;
19
+}
20
+
21
+.sp-container.sp-flat {
22
+	position: relative;
23
+}
24
+
25
+.sp-arrow
26
+{
27
+	position: absolute;
28
+	top: 0;
29
+	left: 106px;
30
+	width: 12px;
31
+	height: 6px;
32
+	background-image: url('../images/resource_arrow_down.svg');
33
+}
34
+
35
+/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */
36
+.sp-top {
37
+  position:relative;
38
+  width: 100%;
39
+  display:inline-block;
40
+}
41
+.sp-top-inner {
42
+   position:absolute;
43
+   top:0;
44
+   left:0;
45
+   bottom:0;
46
+   right:0;
47
+}
48
+.sp-color {
49
+	position: absolute;
50
+	top:0;
51
+	left:0;
52
+	bottom:0;
53
+	right:20%;
54
+}
55
+.sp-hue {
56
+	position: absolute;
57
+	top:0;
58
+	right:0;
59
+	bottom:0;
60
+	left:84%;
61
+	height: 100%;
62
+}
63
+
64
+.sp-clear-enabled .sp-hue {
65
+	top:33px;
66
+	height: 77.5%;
67
+}
68
+
69
+.sp-fill {
70
+	padding-top: 80%;
71
+}
72
+.sp-sat, .sp-val {
73
+	position: absolute;
74
+	top:0;
75
+	left:0;
76
+	right:0;
77
+	bottom:0;
78
+}
79
+
80
+.sp-alpha-enabled .sp-top {
81
+	margin-bottom: 18px;
82
+}
83
+.sp-alpha-enabled .sp-alpha {
84
+	display: block;
85
+}
86
+.sp-alpha-handle {
87
+	position:absolute;
88
+	top:-4px;
89
+	bottom: -4px;
90
+	width: 6px;
91
+	left: 50%;
92
+	cursor: pointer;
93
+	border: 1px solid black;
94
+	background: white;
95
+	opacity: .8;
96
+}
97
+.sp-alpha {
98
+	display: none;
99
+	position: absolute;
100
+	bottom: -14px;
101
+	right: 0;
102
+	left: 0;
103
+	height: 8px;
104
+}
105
+.sp-alpha-inner {
106
+	border: solid 1px #333;
107
+}
108
+
109
+.sp-clear {
110
+	display: none;
111
+}
112
+
113
+.sp-clear.sp-clear-display {
114
+	background-position: center;
115
+}
116
+
117
+.sp-clear-enabled .sp-clear {
118
+	display: block;
119
+	position:absolute;
120
+	top:0px;
121
+	right:0;
122
+	bottom:0;
123
+	left:84%;
124
+	height: 28px;
125
+}
126
+
127
+/* Don't allow text selection */
128
+.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button  {
129
+	cursor:default;
130
+	-webkit-user-select:none;
131
+	-moz-user-select: -moz-none;
132
+	-o-user-select:none;
133
+	user-select: none;
134
+}
135
+
136
+.sp-container.sp-input-disabled .sp-input-container {
137
+	display: none;
138
+}
139
+.sp-container.sp-buttons-disabled .sp-button-container {
140
+	display: none;
141
+}
142
+.sp-palette-only .sp-picker-container {
143
+	display: none;
144
+}
145
+.sp-palette-disabled .sp-palette-container {
146
+	display: none;
147
+}
148
+
149
+.sp-initial-disabled .sp-initial {
150
+	display: none;
151
+}
152
+
153
+
154
+/* Gradients for hue, saturation and value instead of images.  Not pretty... but it works */
155
+.sp-sat {
156
+	background-image: -webkit-gradient(linear,  0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0)));
157
+	background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0));
158
+	background-image: -moz-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
159
+	background-image: -o-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
160
+	background-image: -ms-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
161
+	background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0));
162
+	-ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)";
163
+	filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81');
164
+}
165
+.sp-val {
166
+	background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0)));
167
+	background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0));
168
+	background-image: -moz-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
169
+	background-image: -o-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
170
+	background-image: -ms-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
171
+	background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0));
172
+	-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)";
173
+	filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000');
174
+}
175
+
176
+.sp-hue {
177
+	background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
178
+	background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
179
+	background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
180
+	background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000));
181
+	background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
182
+}
183
+
184
+/* IE filters do not support multiple color stops.
185
+   Generate 6 divs, line them up, and do two color gradients for each.
186
+   Yes, really.
187
+ */
188
+.sp-1 {
189
+	height:17%;
190
+	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00');
191
+}
192
+.sp-2 {
193
+	height:16%;
194
+	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00');
195
+}
196
+.sp-3 {
197
+	height:17%;
198
+	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff');
199
+}
200
+.sp-4 {
201
+	height:17%;
202
+	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff');
203
+}
204
+.sp-5 {
205
+	height:16%;
206
+	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff');
207
+}
208
+.sp-6 {
209
+	height:17%;
210
+	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000');
211
+}
212
+
213
+.sp-hidden {
214
+	display: none !important;
215
+}
216
+
217
+/* Clearfix hack */
218
+.sp-cf:before, .sp-cf:after { content: ""; display: table; }
219
+.sp-cf:after { clear: both; }
220
+.sp-cf { *zoom: 1; }
221
+
222
+/* Mobile devices, make hue slider bigger so it is easier to slide */
223
+@media (max-device-width: 480px) {
224
+	.sp-color { right: 40%; }
225
+	.sp-hue { left: 63%; }
226
+	.sp-fill { padding-top: 60%; }
227
+}
228
+.sp-dragger {
229
+   border-radius: 5px;
230
+   height: 5px;
231
+   width: 5px;
232
+   border: 1px solid #fff;
233
+   background: #000;
234
+   cursor: pointer;
235
+   position:absolute;
236
+   top:0;
237
+   left: 0;
238
+}
239
+.sp-slider {
240
+	position: absolute;
241
+	top:0;
242
+	cursor:pointer;
243
+	height: 3px;
244
+	left: -1px;
245
+	right: -1px;
246
+	border: 1px solid #000;
247
+	background: white;
248
+	opacity: .8;
249
+}
250
+
251
+/*
252
+Theme authors:
253
+Here are the basic themeable display options (colors, fonts, global widths).
254
+See http://bgrins.github.io/spectrum/themes/ for instructions.
255
+*/
256
+
257
+.sp-container {
258
+	border-radius: 0;
259
+	background-color: #f0f0f0;
260
+	padding: 0;
261
+}
262
+.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear
263
+{
264
+	-webkit-box-sizing: border-box;
265
+	-moz-box-sizing: border-box;
266
+	-ms-box-sizing: border-box;
267
+	box-sizing: border-box;
268
+}
269
+
270
+.sp-top
271
+{
272
+	margin-bottom: 3px;
273
+}
274
+
275
+.sp-color, .sp-hue, .sp-clear
276
+{
277
+	border: solid 1px #666;
278
+}
279
+
280
+/* Input */
281
+/*.sp-input-container {
282
+	float:right;
283
+	width: 100px;
284
+	margin-bottom: 4px;
285
+}
286
+.sp-initial-disabled  .sp-input-container {
287
+	width: 100%;
288
+}
289
+.sp-input {
290
+   font-size: 12px !important;
291
+   border: 1px inset;
292
+   padding: 4px 5px;
293
+   margin: 0;
294
+   width: 100%;
295
+   background:transparent;
296
+   border-radius: 3px;
297
+   color: #222;
298
+}
299
+.sp-input:focus  {
300
+	border: 1px solid orange;
301
+}*/
302
+.sp-input-container {
303
+	width: 100%;
304
+	margin-bottom: 4px;
305
+}
306
+.sp-input {
307
+   padding: 4px 5px;
308
+   margin: 0;
309
+   width: 100%;
310
+}
311
+/*.sp-input.sp-validation-error
312
+{
313
+	border: 1px solid red;
314
+	background: #fdd;
315
+}*/
316
+.sp-picker-container , .sp-palette-container
317
+{
318
+	float:left;
319
+	position: relative;
320
+	padding: 10px;
321
+}
322
+.sp-picker-container
323
+{
324
+	width: 204px;
325
+	/*width: 172px;*/
326
+}
327
+
328
+/* Palettes */
329
+.sp-palette-container
330
+{
331
+	border-right: solid 1px #ccc;
332
+}
333
+
334
+.sp-palette .sp-thumb-el {
335
+	display: block;
336
+	position:relative;
337
+	float:left;
338
+	width: 24px;
339
+	height: 15px;
340
+	margin: 3px;
341
+	cursor: pointer;
342
+	border:solid 2px transparent;
343
+}
344
+.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active {
345
+	border-color: orange;
346
+}
347
+.sp-thumb-el
348
+{
349
+	position:relative;
350
+}
351
+
352
+/* Initial */
353
+.sp-initial
354
+{
355
+	float: left;
356
+	border: solid 1px #333;
357
+}
358
+.sp-initial span {
359
+	width: 30px;
360
+	height: 25px;
361
+	border:none;
362
+	display:block;
363
+	float:left;
364
+	margin:0;
365
+}
366
+
367
+.sp-initial .sp-clear-display {
368
+	background-position: center;
369
+}
370
+
371
+/* Buttons */
372
+.sp-button-container {
373
+	width: 100%;
374
+}
375
+
376
+/* Replacer (the little preview div that shows up instead of the <input>) */
377
+.sp-replacer {
378
+	margin:0;
379
+	overflow:hidden;
380
+	cursor:pointer;
381
+	padding: 4px;
382
+	/*display:inline-block;*/
383
+	display: none;
384
+	*zoom: 1;
385
+	*display: inline;
386
+	border: solid 1px #91765d;
387
+	background: #eee;
388
+	color: #333;
389
+	vertical-align: middle;
390
+}
391
+.sp-replacer:hover, .sp-replacer.sp-active {
392
+	border-color: #F0C49B;
393
+	color: #111;
394
+}
395
+.sp-replacer.sp-disabled {
396
+	cursor:default;
397
+	border-color: silver;
398
+	color: silver;
399
+}
400
+.sp-dd {
401
+	padding: 2px 0;
402
+	height: 16px;
403
+	line-height: 16px;
404
+	float:left;
405
+	font-size:10px;
406
+}
407
+.sp-preview
408
+{
409
+	position:relative;
410
+	width:25px;
411
+	height: 20px;
412
+	border: solid 1px #222;
413
+	margin-right: 5px;
414
+	float:left;
415
+	z-index: 0;
416
+}
417
+
418
+.sp-palette
419
+{
420
+	*width: 220px;
421
+	max-width: 220px;
422
+}
423
+.sp-palette .sp-thumb-el
424
+{
425
+	width:16px;
426
+	height: 16px;
427
+	margin:2px 1px;
428
+	border: solid 1px #d0d0d0;
429
+}
430
+
431
+.sp-container
432
+{
433
+	padding-bottom:0;
434
+}
435
+
436
+
437
+/* Buttons: http://hellohappy.org/css3-buttons/ */
438
+/*.sp-container button {
439
+  background-color: #eeeeee;
440
+  background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc);
441
+  background-image: -moz-linear-gradient(top, #eeeeee, #cccccc);
442
+  background-image: -ms-linear-gradient(top, #eeeeee, #cccccc);
443
+  background-image: -o-linear-gradient(top, #eeeeee, #cccccc);
444
+  background-image: linear-gradient(to bottom, #eeeeee, #cccccc);
445
+  border: 1px solid #ccc;
446
+  border-bottom: 1px solid #bbb;
447
+  border-radius: 3px;
448
+  color: #333;
449
+  font-size: 14px;
450
+  line-height: 1;
451
+  padding: 5px 4px;
452
+  text-align: center;
453
+  text-shadow: 0 1px 0 #eee;
454
+  vertical-align: middle;
455
+}
456
+.sp-container button:hover {
457
+	background-color: #dddddd;
458
+	background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb);
459
+	background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb);
460
+	background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb);
461
+	background-image: -o-linear-gradient(top, #dddddd, #bbbbbb);
462
+	background-image: linear-gradient(to bottom, #dddddd, #bbbbbb);
463
+	border: 1px solid #bbb;
464
+	border-bottom: 1px solid #999;
465
+	cursor: pointer;
466
+	text-shadow: 0 1px 0 #ddd;
467
+}
468
+.sp-container button:active {
469
+	border: 1px solid #aaa;
470
+	border-bottom: 1px solid #888;
471
+	-webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
472
+	-moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
473
+	-ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
474
+	-o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
475
+	box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
476
+}
477
+.sp-cancel
478
+{
479
+	font-size: 11px;
480
+	color: #d93f3f !important;
481
+	margin:0;
482
+	padding:2px;
483
+	margin-right: 5px;
484
+	vertical-align: middle;
485
+	text-decoration:none;
486
+
487
+}
488
+.sp-cancel:hover
489
+{
490
+	color: #d93f3f !important;
491
+	text-decoration: underline;
492
+}*/
493
+
494
+.sp-container input[type=button]
495
+{
496
+	width: 40%;
497
+}
498
+
499
+.sp-cancel
500
+{
501
+	float: right;
502
+}
503
+
504
+.sp-palette span:hover, .sp-palette span.sp-thumb-active
505
+{
506
+	border-color: #000;
507
+}
508
+
509
+.sp-preview, .sp-alpha, .sp-thumb-el
510
+{
511
+	position:relative;
512
+	background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
513
+}
514
+.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner
515
+{
516
+	display:block;
517
+	position:absolute;
518
+	top:0;left:0;bottom:0;right:0;
519
+}
520
+
521
+.sp-palette .sp-thumb-inner
522
+{
523
+	background-position: 50% 50%;
524
+	background-repeat: no-repeat;
525
+}
526
+
527
+.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner
528
+{
529
+	background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=);
530
+}
531
+
532
+.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner
533
+{
534
+	background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=);
535
+}
536
+
537
+.sp-clear-display {
538
+	background-repeat:no-repeat;
539
+	background-position: center;
540
+	background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==);
541
+}
542
+
543
+.sp-inverse.sp-container {
544
+	border-top: 1px solid #c0c0c0;
545
+	border-bottom: none;
546
+}
547
+
548
+.sp-inverse .sp-arrow
549
+{
550
+	top: auto;
551
+	bottom: 0;
552
+	background-image: url('../images/resource_arrow_up.svg');
553
+}

文件差異過大導致無法顯示
+ 4265 - 0
tracim/tracim/public/caldavzap/data_process.js


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Bold-webfont.eot 查看文件


文件差異過大導致無法顯示
+ 7496 - 0
tracim/tracim/public/caldavzap/fonts/Roboto-Bold-webfont.svg


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Bold-webfont.ttf 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Bold-webfont.woff 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-BoldItalic-webfont.eot 查看文件


文件差異過大導致無法顯示
+ 8652 - 0
tracim/tracim/public/caldavzap/fonts/Roboto-BoldItalic-webfont.svg


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-BoldItalic-webfont.ttf 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-BoldItalic-webfont.woff 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Italic-webfont.eot 查看文件


文件差異過大導致無法顯示
+ 8164 - 0
tracim/tracim/public/caldavzap/fonts/Roboto-Italic-webfont.svg


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Italic-webfont.ttf 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Italic-webfont.woff 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Light-webfont.eot 查看文件


文件差異過大導致無法顯示
+ 8162 - 0
tracim/tracim/public/caldavzap/fonts/Roboto-Light-webfont.svg


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Light-webfont.ttf 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Light-webfont.woff 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-LightItalic-webfont.eot 查看文件


文件差異過大導致無法顯示
+ 8162 - 0
tracim/tracim/public/caldavzap/fonts/Roboto-LightItalic-webfont.svg


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-LightItalic-webfont.ttf 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-LightItalic-webfont.woff 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Medium-webfont.eot 查看文件


文件差異過大導致無法顯示
+ 7496 - 0
tracim/tracim/public/caldavzap/fonts/Roboto-Medium-webfont.svg


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Medium-webfont.ttf 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Medium-webfont.woff 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-MediumItalic-webfont.eot 查看文件


文件差異過大導致無法顯示
+ 8652 - 0
tracim/tracim/public/caldavzap/fonts/Roboto-MediumItalic-webfont.svg


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-MediumItalic-webfont.ttf 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-MediumItalic-webfont.woff 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Regular-webfont.eot 查看文件


文件差異過大導致無法顯示
+ 7606 - 0
tracim/tracim/public/caldavzap/fonts/Roboto-Regular-webfont.svg


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Regular-webfont.ttf 查看文件


二進制
tracim/tracim/public/caldavzap/fonts/Roboto-Regular-webfont.woff 查看文件


+ 202 - 0
tracim/tracim/public/caldavzap/fonts/license.txt 查看文件

@@ -0,0 +1,202 @@
1
+
2
+                                 Apache License
3
+                           Version 2.0, January 2004
4
+                        http://www.apache.org/licenses/
5
+
6
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+   1. Definitions.
9
+
10
+      "License" shall mean the terms and conditions for use, reproduction,
11
+      and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+      "Licensor" shall mean the copyright owner or entity authorized by
14
+      the copyright owner that is granting the License.
15
+
16
+      "Legal Entity" shall mean the union of the acting entity and all
17
+      other entities that control, are controlled by, or are under common
18
+      control with that entity. For the purposes of this definition,
19
+      "control" means (i) the power, direct or indirect, to cause the
20
+      direction or management of such entity, whether by contract or
21
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+      outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+      "You" (or "Your") shall mean an individual or Legal Entity
25
+      exercising permissions granted by this License.
26
+
27
+      "Source" form shall mean the preferred form for making modifications,
28
+      including but not limited to software source code, documentation
29
+      source, and configuration files.
30
+
31
+      "Object" form shall mean any form resulting from mechanical
32
+      transformation or translation of a Source form, including but
33
+      not limited to compiled object code, generated documentation,
34
+      and conversions to other media types.
35
+
36
+      "Work" shall mean the work of authorship, whether in Source or
37
+      Object form, made available under the License, as indicated by a
38
+      copyright notice that is included in or attached to the work
39
+      (an example is provided in the Appendix below).
40
+
41
+      "Derivative Works" shall mean any work, whether in Source or Object
42
+      form, that is based on (or derived from) the Work and for which the
43
+      editorial revisions, annotations, elaborations, or other modifications
44
+      represent, as a whole, an original work of authorship. For the purposes
45
+      of this License, Derivative Works shall not include works that remain
46
+      separable from, or merely link (or bind by name) to the interfaces of,
47
+      the Work and Derivative Works thereof.
48
+
49
+      "Contribution" shall mean any work of authorship, including
50
+      the original version of the Work and any modifications or additions
51
+      to that Work or Derivative Works thereof, that is intentionally
52
+      submitted to Licensor for inclusion in the Work by the copyright owner
53
+      or by an individual or Legal Entity authorized to submit on behalf of
54
+      the copyright owner. For the purposes of this definition, "submitted"
55
+      means any form of electronic, verbal, or written communication sent
56
+      to the Licensor or its representatives, including but not limited to
57
+      communication on electronic mailing lists, source code control systems,
58
+      and issue tracking systems that are managed by, or on behalf of, the
59
+      Licensor for the purpose of discussing and improving the Work, but
60
+      excluding communication that is conspicuously marked or otherwise
61
+      designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+      "Contributor" shall mean Licensor and any individual or Legal Entity
64
+      on behalf of whom a Contribution has been received by Licensor and
65
+      subsequently incorporated within the Work.
66
+
67
+   2. Grant of Copyright License. Subject to the terms and conditions of
68
+      this License, each Contributor hereby grants to You a perpetual,
69
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+      copyright license to reproduce, prepare Derivative Works of,
71
+      publicly display, publicly perform, sublicense, and distribute the
72
+      Work and such Derivative Works in Source or Object form.
73
+
74
+   3. Grant of Patent License. Subject to the terms and conditions of
75
+      this License, each Contributor hereby grants to You a perpetual,
76
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+      (except as stated in this section) patent license to make, have made,
78
+      use, offer to sell, sell, import, and otherwise transfer the Work,
79
+      where such license applies only to those patent claims licensable
80
+      by such Contributor that are necessarily infringed by their
81
+      Contribution(s) alone or by combination of their Contribution(s)
82
+      with the Work to which such Contribution(s) was submitted. If You
83
+      institute patent litigation against any entity (including a
84
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+      or a Contribution incorporated within the Work constitutes direct
86
+      or contributory patent infringement, then any patent licenses
87
+      granted to You under this License for that Work shall terminate
88
+      as of the date such litigation is filed.
89
+
90
+   4. Redistribution. You may reproduce and distribute copies of the
91
+      Work or Derivative Works thereof in any medium, with or without
92
+      modifications, and in Source or Object form, provided that You
93
+      meet the following conditions:
94
+
95
+      (a) You must give any other recipients of the Work or
96
+          Derivative Works a copy of this License; and
97
+
98
+      (b) You must cause any modified files to carry prominent notices
99
+          stating that You changed the files; and
100
+
101
+      (c) You must retain, in the Source form of any Derivative Works
102
+          that You distribute, all copyright, patent, trademark, and
103
+          attribution notices from the Source form of the Work,
104
+          excluding those notices that do not pertain to any part of
105
+          the Derivative Works; and
106
+
107
+      (d) If the Work includes a "NOTICE" text file as part of its
108
+          distribution, then any Derivative Works that You distribute must
109
+          include a readable copy of the attribution notices contained
110
+          within such NOTICE file, excluding those notices that do not
111
+          pertain to any part of the Derivative Works, in at least one
112
+          of the following places: within a NOTICE text file distributed
113
+          as part of the Derivative Works; within the Source form or
114
+          documentation, if provided along with the Derivative Works; or,
115
+          within a display generated by the Derivative Works, if and
116
+          wherever such third-party notices normally appear. The contents
117
+          of the NOTICE file are for informational purposes only and
118
+          do not modify the License. You may add Your own attribution
119
+          notices within Derivative Works that You distribute, alongside
120
+          or as an addendum to the NOTICE text from the Work, provided
121
+          that such additional attribution notices cannot be construed
122
+          as modifying the License.
123
+
124
+      You may add Your own copyright statement to Your modifications and
125
+      may provide additional or different license terms and conditions
126
+      for use, reproduction, or distribution of Your modifications, or
127
+      for any such Derivative Works as a whole, provided Your use,
128
+      reproduction, and distribution of the Work otherwise complies with
129
+      the conditions stated in this License.
130
+
131
+   5. Submission of Contributions. Unless You explicitly state otherwise,
132
+      any Contribution intentionally submitted for inclusion in the Work
133
+      by You to the Licensor shall be under the terms and conditions of
134
+      this License, without any additional terms or conditions.
135
+      Notwithstanding the above, nothing herein shall supersede or modify
136
+      the terms of any separate license agreement you may have executed
137
+      with Licensor regarding such Contributions.
138
+
139
+   6. Trademarks. This License does not grant permission to use the trade
140
+      names, trademarks, service marks, or product names of the Licensor,
141
+      except as required for reasonable and customary use in describing the
142
+      origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+   7. Disclaimer of Warranty. Unless required by applicable law or
145
+      agreed to in writing, Licensor provides the Work (and each
146
+      Contributor provides its Contributions) on an "AS IS" BASIS,
147
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+      implied, including, without limitation, any warranties or conditions
149
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+      PARTICULAR PURPOSE. You are solely responsible for determining the
151
+      appropriateness of using or redistributing the Work and assume any
152
+      risks associated with Your exercise of permissions under this License.
153
+
154
+   8. Limitation of Liability. In no event and under no legal theory,
155
+      whether in tort (including negligence), contract, or otherwise,
156
+      unless required by applicable law (such as deliberate and grossly
157
+      negligent acts) or agreed to in writing, shall any Contributor be
158
+      liable to You for damages, including any direct, indirect, special,
159
+      incidental, or consequential damages of any character arising as a
160
+      result of this License or out of the use or inability to use the
161
+      Work (including but not limited to damages for loss of goodwill,
162
+      work stoppage, computer failure or malfunction, or any and all
163
+      other commercial damages or losses), even if such Contributor
164
+      has been advised of the possibility of such damages.
165
+
166
+   9. Accepting Warranty or Additional Liability. While redistributing
167
+      the Work or Derivative Works thereof, You may choose to offer,
168
+      and charge a fee for, acceptance of support, warranty, indemnity,
169
+      or other liability obligations and/or rights consistent with this
170
+      License. However, in accepting such obligations, You may act only
171
+      on Your own behalf and on Your sole responsibility, not on behalf
172
+      of any other Contributor, and only if You agree to indemnify,
173
+      defend, and hold each Contributor harmless for any liability
174
+      incurred by, or claims asserted against, such Contributor by reason
175
+      of your accepting any such warranty or additional liability.
176
+
177
+   END OF TERMS AND CONDITIONS
178
+
179
+   APPENDIX: How to apply the Apache License to your work.
180
+
181
+      To apply the Apache License to your work, attach the following
182
+      boilerplate notice, with the fields enclosed by brackets "[]"
183
+      replaced with your own identifying information. (Don't include
184
+      the brackets!)  The text should be enclosed in the appropriate
185
+      comment syntax for the file format. We also recommend that a
186
+      file or class name and description of purpose be included on the
187
+      same "printed page" as the copyright notice for easier
188
+      identification within third-party archives.
189
+
190
+   Copyright [yyyy] [name of copyright owner]
191
+
192
+   Licensed under the Apache License, Version 2.0 (the "License");
193
+   you may not use this file except in compliance with the License.
194
+   You may obtain a copy of the License at
195
+
196
+       http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+   Unless required by applicable law or agreed to in writing, software
199
+   distributed under the License is distributed on an "AS IS" BASIS,
200
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+   See the License for the specific language governing permissions and
202
+   limitations under the License.

文件差異過大導致無法顯示
+ 3307 - 0
tracim/tracim/public/caldavzap/forms.js


+ 0 - 0
tracim/tracim/public/caldavzap/images/add_cal.svg 查看文件


部分文件因文件數量過多而無法顯示