Compare commits

...

246 Commits

Author SHA1 Message Date
David Benque 1401925f5a Merge remote-tracking branch 'origin/main' 2022-12-21 14:34:17 +00:00
David Benque fea4f179d1 Bmp version number 2022-12-21 14:29:00 +00:00
David Benque 3cfccf8024 Changelog for 5.2.1 2022-12-21 13:55:26 +00:00
yflory 6eab023a5a Merge branch 'soon' of github.com:xwiki-labs/cryptpad into soon 2022-12-21 13:34:13 +01:00
yflory 4709b6740d Fix link location in calendar events 2022-12-21 13:34:01 +01:00
yflory d5fefb5946 Fix 'delete all' button in forms owned by a team 2022-12-21 13:28:29 +01:00
Weblate 0e1ff02a93 Translated using Weblate (Russian)
Currently translated at 99.6% (1573 of 1578 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/ru/

Translated using Weblate (Russian)

Currently translated at 99.6% (1573 of 1578 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/ru/
2022-12-21 12:41:58 +01:00
yflory f01e21eac7 Merge branch 'soon' of github.com:xwiki-labs/cryptpad into soon 2022-12-19 16:11:06 +01:00
yflory 8245189bcb Fix NO_SUCH_CHANNEL error after using the debug app 2022-12-19 16:10:42 +01:00
Weblate 1715250677 Translated using Weblate (German)
Currently translated at 100.0% (1578 of 1578 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/
2022-12-19 15:26:08 +01:00
Weblate 20a4c8114e Translated using Weblate (French)
Currently translated at 100.0% (1578 of 1578 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-12-19 15:20:54 +01:00
Weblate 7d8fabb63c Translated using Weblate (Russian)
Currently translated at 99.6% (1572 of 1577 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/ru/
2022-12-19 15:19:40 +01:00
Weblate 5b5c11fcfb Translated using Weblate (English)
Currently translated at 100.0% (1578 of 1578 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/
2022-12-19 15:19:40 +01:00
Weblate e6d4b75a75 Translated using Weblate (Hindi)
Currently translated at 0.6% (11 of 1577 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/hi/
2022-12-19 15:19:40 +01:00
yflory 770fb3d657 Fix drag&drop a file from a shared folder into templates 2022-12-19 14:47:19 +01:00
yflory 74707ea723 Fix ownership issues with forms created from the drive 2022-12-19 11:50:36 +01:00
ansuz 2834842f3a
Merge pull request #987 from cremesk/main
relax HSTS header checkup to only require a max age
2022-12-19 14:59:21 +05:30
creme 940d7d3118
relex HSTS header checkup to only require a max age
Signed-off-by: creme <creme@envs.net>
2022-12-16 17:44:02 +01:00
David Benque a7463c1987 Add comments to pass translations test 2022-12-16 12:05:01 +00:00
David Benque 372e0dd3e6 Add new test to the changelog 2022-12-16 11:26:06 +00:00
ansuz 0231fc684d fix a typo in the newest checkup test 2022-12-16 15:58:19 +05:30
ansuz 505b42f740 check that hosts provide an HSTS header 2022-12-16 15:52:30 +05:30
yflory 75de90c8b5 Send form notification anonymously 2022-12-15 16:22:38 +01:00
Weblate de240c3af4 Translated using Weblate (German)
Currently translated at 100.0% (1577 of 1577 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/
2022-12-15 16:08:31 +01:00
Weblate 519204ceae Translated using Weblate (English)
Currently translated at 100.0% (1577 of 1577 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/
2022-12-15 16:08:31 +01:00
Weblate 382e49e373 Translated using Weblate (French)
Currently translated at 100.0% (1577 of 1577 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/

Translated using Weblate (French)

Currently translated at 100.0% (1576 of 1576 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-12-15 16:08:31 +01:00
David Benque 4c1850d0a6 Remove hard coded string 2022-12-15 15:05:29 +00:00
Weblate dd6d2fa959 Translated using Weblate (French)
Currently translated at 100.0% (1576 of 1576 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-12-15 15:59:04 +01:00
Mathilde Grünig ece69b8c37 update Nginx requirements on CHANGELOG 2022-12-15 13:28:48 +01:00
David Benque 1af5c08bfa Merge branch '5.2-candidate' into soon 2022-12-15 09:34:09 +00:00
Weblate 9a63c3b2ab Translated using Weblate (German)
Currently translated at 100.0% (1576 of 1576 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/

Translated using Weblate (German)

Currently translated at 100.0% (1576 of 1576 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/
2022-12-15 10:21:25 +01:00
David Benque 6f4d922be2 Tweak team invite link modal 2022-12-14 14:37:33 +00:00
David Benque ebf103bb4e Update changelog 2022-12-14 14:10:51 +00:00
ansuz 4aaa2bd71c Merge branch '5.2-candidate' into soon 2022-12-14 14:57:13 +05:30
Weblate b3cbf34bc2 Translated using Weblate (French)
Currently translated at 100.0% (1576 of 1576 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-12-13 23:37:42 +01:00
David Benque bf07056bab Draft changelog 2022-12-13 20:19:36 +00:00
David Benque 839bc57849 Merge branch 'soon' into 5.2-candidate 2022-12-13 11:43:14 +00:00
Weblate 61589934d7 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/
2022-12-13 12:41:11 +01:00
David Benque 391628510f Remove rogue translation string 2022-12-13 11:40:52 +00:00
David Benque 8a2f71a573 Merge remote-tracking branch 'origin/5.2-candidate' into 5.2-candidate 2022-12-13 11:39:12 +00:00
David Benque dc91ecd78d Remove XXX 2022-12-13 11:39:03 +00:00
David Benque 4f384c4b89 Bump version 2022-12-13 11:37:57 +00:00
Weblate 109724def5 Translated using Weblate (English)
Currently translated at 100.0% (1577 of 1577 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/
2022-12-13 12:33:11 +01:00
Weblate 82a5696f41 Translated using Weblate (Basque)
Currently translated at 98.8% (1559 of 1577 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/eu/
2022-12-13 12:28:09 +01:00
ansuz a804a0d9bc Merge branch 'staging' into 5.2-candidate 2022-12-13 16:49:55 +05:30
Weblate 30de40cdbc Translated using Weblate (English)
Currently translated at 100.0% (1577 of 1577 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/
2022-12-13 12:17:13 +01:00
Weblate aa7dcfdf26 Translated using Weblate (French)
Currently translated at 100.0% (1577 of 1577 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/

Translated using Weblate (French)

Currently translated at 100.0% (1576 of 1576 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-12-13 12:17:13 +01:00
ansuz f39bcb2806 Merge branch 'soon' into staging 2022-12-13 16:43:26 +05:30
ansuz 9a5b54d091 Merge branch 'main' into soon 2022-12-13 16:43:16 +05:30
ansuz 78ba1a7f18
Merge pull request #972 from dbkr/patch-1
Make unparseable message non-fatal
2022-12-13 16:26:21 +05:30
ansuz b29463c4da
Merge pull request #986 from xwiki-labs/dependabot/npm_and_yarn/qs-6.5.3
Bump qs from 6.5.2 to 6.5.3
2022-12-13 16:24:47 +05:30
dependabot[bot] 64407f7de6
Bump qs from 6.5.2 to 6.5.3
Bumps [qs](https://github.com/ljharb/qs) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/ljharb/qs/releases)
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.5.2...v6.5.3)

---
updated-dependencies:
- dependency-name: qs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-13 10:40:44 +00:00
ansuz 927a62ad5a more dependency updates 2022-12-13 16:08:48 +05:30
ansuz fbb1f43ede
Merge pull request #983 from xwiki-labs/dependabot/npm_and_yarn/decode-uri-component-0.2.2
Bump decode-uri-component from 0.2.0 to 0.2.2
2022-12-13 16:05:05 +05:30
ansuz 4e1a035ed9
Merge pull request #985 from xwiki-labs/dependabot/npm_and_yarn/express-4.17.3
Bump express from 4.16.4 to 4.17.3
2022-12-13 16:04:49 +05:30
yflory 3fce59aefc Fix TypeError in forms 2022-12-13 10:15:54 +01:00
Weblate 5df44b10cc Translated using Weblate (French)
Currently translated at 100.0% (1576 of 1576 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-12-12 17:11:28 +01:00
Weblate 2d46de5ad5 Translated using Weblate (French)
Currently translated at 100.0% (1576 of 1576 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-12-12 17:06:32 +01:00
yflory 5f6f4edf19 Merge branch '5.2-candidate' of github.com:xwiki-labs/cryptpad into 5.2-candidate 2022-12-12 16:56:21 +01:00
yflory b6274dbf0c Move form export options in dropdown 2022-12-12 16:55:03 +01:00
David Benque 81a7fed01f Remove more temporary translation strings 2022-12-12 15:50:06 +00:00
David Benque c35cd6a5f6 Merge branch 'soon' into 5.2-candidate 2022-12-12 15:46:16 +00:00
Weblate c0abc82983 Translated using Weblate (Japanese)
Currently translated at 98.7% (1554 of 1574 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/ja/
2022-12-12 16:44:56 +01:00
Weblate 46db0213dc Translated using Weblate (English)
Currently translated at 100.0% (1576 of 1576 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1576 of 1576 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1575 of 1575 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/
2022-12-12 16:44:56 +01:00
Weblate 6ec43e91ca Translated using Weblate (French)
Currently translated at 100.0% (1574 of 1574 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-12-12 16:44:56 +01:00
David Benque 8995255932 Merge branch 'soon' into 5.2-candidate 2022-12-12 15:35:35 +00:00
David Benque 4cffb0d1be Merge remote-tracking branch 'origin/5.2-candidate' into 5.2-candidate 2022-12-12 15:35:03 +00:00
David Benque 830281fd68 Remove temporary translation strings 2022-12-12 15:34:50 +00:00
yflory 741bd300af Merge branch 'staging' into 5.2-candidate 2022-12-12 16:32:15 +01:00
yflory 83744740e5 Merge branch 'form-del-mult' into 5.2-candidate 2022-12-12 16:31:50 +01:00
yflory 4434cdca17 Merge branch 'team-onboarding' into 5.2-candidate 2022-12-12 16:31:39 +01:00
yflory 5521db2441 Merge branch 'team-onboarding' of github.com:xwiki-labs/cryptpad into team-onboarding 2022-12-12 15:52:40 +01:00
yflory ab1695900c Improve team invite modal UI 2022-12-12 15:52:32 +01:00
David Benque cc852cc505 Adjust UI for team invite links 2022-12-12 14:52:12 +00:00
yflory 1eaa09df3f Merge branch 'form-del-mult' of github.com:xwiki-labs/cryptpad into form-del-mult 2022-12-12 14:13:22 +01:00
yflory f1ff39fa4f Don't show form notification settings for non-owner 2022-12-12 14:13:08 +01:00
David Benque 3d9ec26fcb Form submit message UI 2022-12-12 11:56:43 +00:00
David Benque 7563fc3d92 Improve form settings readability
- add color to tiles that have a separate status
- changed order of settings
2022-12-12 11:31:50 +00:00
dependabot[bot] 0181e28979
Bump express from 4.16.4 to 4.17.3
Bumps [express](https://github.com/expressjs/express) from 4.16.4 to 4.17.3.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.16.4...4.17.3)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-10 00:46:02 +00:00
Weblate 034e4b53b5 Translated using Weblate (English)
Currently translated at 100.0% (1574 of 1574 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1573 of 1573 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1572 of 1572 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1571 of 1571 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1570 of 1570 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1569 of 1569 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1568 of 1568 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1567 of 1567 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1566 of 1566 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1565 of 1565 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1564 of 1564 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1563 of 1563 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1562 of 1562 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1561 of 1561 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/
2022-12-09 12:11:17 +01:00
Weblate b6a272a1df Translated using Weblate (Basque)
Currently translated at 99.9% (1559 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/eu/

Translated using Weblate (Basque)

Currently translated at 97.8% (1526 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/eu/

Translated using Weblate (Basque)

Currently translated at 96.1% (1500 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/eu/

Translated using Weblate (Basque)

Currently translated at 95.5% (1491 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/eu/
2022-12-09 11:57:38 +01:00
Weblate 97390d0da4 Translated using Weblate (Spanish)
Currently translated at 100.0% (1560 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/es/
2022-12-09 11:57:38 +01:00
Weblate cd622d1c62 Translated using Weblate (French)
Currently translated at 100.0% (1560 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-12-09 11:57:38 +01:00
David Benque f535c778e3 Merge remote-tracking branch 'origin/form-del-mult' into form-del-mult 2022-12-09 10:18:47 +00:00
David Benque 38c3515fbb Mark translation keys copied 2022-12-09 10:15:22 +00:00
yflory 55f3b910ee Automatically refresh number of form responses 2022-12-08 17:20:50 +01:00
yflory 97fde60e84 Delete only selected answers in forms 2022-12-08 17:20:31 +01:00
yflory 600771682a Allow edit/delete/multiple answers without a drive and fix race condition 2022-12-08 16:53:29 +01:00
yflory f0bc1ef07a Fix missing button in forms 2022-12-07 17:42:44 +01:00
yflory 6e555c73b4 Merge branch 'team-onboarding' of github.com:xwiki-labs/cryptpad into team-onboarding 2022-12-07 15:04:08 +01:00
yflory ad0c2e90dc Fix UI issues with team invitation links 2022-12-07 15:03:55 +01:00
yflory c074eab7b7 Merge branch '5.2-candidate' into form-del-mult 2022-12-07 14:45:18 +01:00
yflory 99011f305a Remove debug log 2022-12-07 14:44:35 +01:00
yflory b2887a5d69 Update form UI and fix owner issues 2022-12-07 14:12:13 +01:00
Mathilde Grünig 37ccaddbbe 2nd thought on HTTP/80, not needed in the end 2022-12-07 14:04:00 +01:00
David Benque 16bced389a Fix confirm button timer regression 2022-12-07 14:01:23 +01:00
Mathilde Grünig 1b731e2643 Add future-proof Nginx configuration
- support IPv6
- 80 to 443 redirect
- TLS generation
- better SSL sessions
- longer HSTS (2 years)
- OCSP stapling
2022-12-07 13:56:12 +01:00
David Benque 3d0d4f342d Fix broken links to docs in team invite form 2022-12-07 10:19:33 +01:00
dependabot[bot] ec753b0be0
Bump decode-uri-component from 0.2.0 to 0.2.2
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-07 08:38:53 +00:00
yflory 27e6f9a34b Fix overflowing text in forms 2022-12-06 15:32:28 +01:00
David Benque 0184030f18 Fix swap axes button in Poll 2022-12-06 11:35:13 +01:00
yflory 9ef6ea4dff Merge branch 'staging' into 5.2-candidate 2022-12-05 13:55:02 +01:00
yflory d03d06e3c2 Fix transfer ownership before storing a pad in the drive 2022-12-05 13:54:38 +01:00
yflory 112d3a04bd Fix form style issues with long text 2022-12-05 13:52:41 +01:00
yflory 59c13c506c Allow form author to mute the channel 2022-11-30 18:41:16 +01:00
yflory 119efa1180 Fix missing entries in form responses 2022-11-30 17:30:24 +01:00
Weblate 92aeae1219 Translated using Weblate (Chinese (Simplified))
Currently translated at 92.3% (1441 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/zh_Hans/
2022-11-28 15:24:36 +01:00
Weblate e3b73d4470 Translated using Weblate (Spanish)
Currently translated at 100.0% (1560 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/es/
2022-11-28 15:24:36 +01:00
Weblate dff0b2e4c3 Added translation using Weblate (Persian) 2022-11-28 15:24:35 +01:00
Weblate 1b66f210c8 Translated using Weblate (French)
Currently translated at 100.0% (1560 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-11-28 15:24:35 +01:00
yflory 15d9a9c703 Fix middle click in drive 'New' menu 2022-11-09 16:39:57 +01:00
yflory fb079c49bf Team invitation link improvements 2022-11-04 16:42:04 +01:00
yflory acd7d9654d Fix calendar issues 2022-11-02 14:58:29 +01:00
Weblate 90611b03a0 Translated using Weblate (German)
Currently translated at 100.0% (1560 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/
2022-11-02 12:40:11 +01:00
Weblate 1204d9df4b Translated using Weblate (Polish)
Currently translated at 89.2% (1392 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/pl/
2022-11-02 12:40:11 +01:00
yflory 4f23dc53c3 Fix form JSON export with multiple responses per user 2022-10-27 15:14:49 +02:00
yflory d896b0dfd6 Fix form issues 2022-10-27 15:10:08 +02:00
yflory f27431a2f8 Merge branch 'form-del' into form-del-mult 2022-10-26 17:57:53 +02:00
yflory 01a2bb7765 Merge branch 'form' into form-del 2022-10-26 17:51:28 +02:00
yflory b8b485c99d Merge branch 'staging' into form 2022-10-26 17:51:17 +02:00
yflory 5d350f1c45 Merge branch 'form' into form-del 2022-10-26 17:51:10 +02:00
yflory e9540b9a00 Fix request pad access 2022-10-26 17:48:24 +02:00
yflory 7eeb6bb165 Keep loading screen until drive is fully renderedw 2022-10-26 17:10:17 +02:00
yflory 1d062c98f2 Merge branch 'soon' into staging 2022-10-26 16:20:01 +02:00
yflory 8d250722fd Merge branch 'main' into soon 2022-10-26 16:19:48 +02:00
yflory 51c50db166 Fix form anonymous responses not triggering the notification 2022-10-26 16:14:45 +02:00
yflory 614d3834a3 Fix editing response with ordered list in forms 2022-10-25 17:15:25 +02:00
yflory 9c34e9e779 Notify form author on new responses 2022-10-25 16:40:15 +02:00
yflory f0a3e0eb81 Export form results as JSON #837 2022-10-25 15:51:23 +02:00
David Benque 89d0a7ef28 Move daily(weekday/weekend) option up the list 2022-10-25 10:42:10 +01:00
yflory 846b91907b Use CryptPad language in dates 2022-10-25 11:07:54 +02:00
yflory c49949f810 Mutiple answers 2022-10-25 11:01:01 +02:00
David Benque 0411adc0b7 Merge branch 'soon' into staging 2022-10-24 14:45:48 +01:00
David Benque f8b06af65b Remove hard-coded translations 2022-10-24 14:45:12 +01:00
Weblate cb26072115 Translated using Weblate (English)
Currently translated at 100.0% (1560 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1560 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1559 of 1559 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1558 of 1558 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1557 of 1557 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1556 of 1556 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/
2022-10-24 15:43:37 +02:00
Weblate 39dc8dbaf6 Translated using Weblate (French)
Currently translated at 100.0% (1560 of 1560 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-10-24 15:43:37 +02:00
Weblate 2ef2cc3517 Translated using Weblate (German)
Currently translated at 99.8% (1552 of 1555 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/

Translated using Weblate (German)

Currently translated at 99.7% (1551 of 1555 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/

Translated using Weblate (German)

Currently translated at 99.6% (1549 of 1555 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/

Translated using Weblate (German)

Currently translated at 99.0% (1541 of 1555 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/

Translated using Weblate (German)

Currently translated at 98.3% (1530 of 1555 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/

Translated using Weblate (German)

Currently translated at 98.3% (1529 of 1555 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/
2022-10-21 21:23:01 +02:00
David Benque 795ec4b22f Remove some hard-coded translations 2022-10-21 15:47:06 +01:00
David Benque d37e3c61e7 Merge branch 'soon' into staging 2022-10-21 15:43:33 +01:00
David Benque b2022f5e73 Merge branch 'rrule' into staging 2022-10-21 15:43:24 +01:00
Weblate e7be485e89 Translated using Weblate (Japanese)
Currently translated at 98.9% (1538 of 1555 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/ja/
2022-10-21 16:16:40 +02:00
Weblate 5d29c0134d Translated using Weblate (English)
Currently translated at 100.0% (1555 of 1555 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1555 of 1555 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1554 of 1554 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1553 of 1553 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1552 of 1552 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1551 of 1551 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1550 of 1550 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1549 of 1549 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1548 of 1548 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1547 of 1547 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1546 of 1546 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1545 of 1545 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1544 of 1544 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1543 of 1543 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1542 of 1542 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1541 of 1541 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1540 of 1540 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1539 of 1539 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1538 of 1538 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1537 of 1537 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1536 of 1536 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1535 of 1535 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1534 of 1534 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1533 of 1533 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1532 of 1532 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1531 of 1531 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1530 of 1530 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1529 of 1529 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1528 of 1528 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1527 of 1527 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1526 of 1526 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1525 of 1525 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1524 of 1524 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1523 of 1523 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1522 of 1522 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1521 of 1521 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1520 of 1520 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1519 of 1519 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1518 of 1518 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1517 of 1517 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1516 of 1516 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1515 of 1515 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1514 of 1514 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1513 of 1513 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1512 of 1512 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1511 of 1511 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1510 of 1510 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1509 of 1509 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1508 of 1508 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1507 of 1507 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1506 of 1506 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1505 of 1505 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1504 of 1504 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1503 of 1503 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/
2022-10-21 16:16:40 +02:00
Weblate e7c0a0c1f6 Translated using Weblate (Spanish)
Currently translated at 100.0% (1555 of 1555 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/es/
2022-10-21 16:16:40 +02:00
Weblate baea5166b0 Translated using Weblate (French)
Currently translated at 100.0% (1555 of 1555 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/

Translated using Weblate (French)

Currently translated at 98.1% (1526 of 1555 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-10-21 16:16:39 +02:00
yflory b605c516f0 Allow form owner to reset answers 2022-10-18 11:12:37 +02:00
yflory 89448115c5 Delete form own answers 2022-10-18 10:19:57 +02:00
David Benque 736d4a531a Remove hard-coded translations 2022-10-17 14:31:14 +02:00
David Benque 87e46dd4ae Merge remote-tracking branch 'origin/staging' into staging 2022-10-17 14:26:58 +02:00
David Benque 1e99283db8 Merge remote-tracking branch 'origin/staging' into staging 2022-10-17 14:25:45 +02:00
David Benque fbb991bfad Merge remote-tracking branch 'origin/soon' into staging 2022-10-17 14:25:32 +02:00
yflory b85801e830 lint compliance 2022-10-17 14:24:49 +02:00
yflory 0e27daffc0 Remove debugging console.log 2022-10-17 14:23:57 +02:00
Weblate 2a67360ee1 Translated using Weblate (English)
Currently translated at 100.0% (1502 of 1502 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/

Translated using Weblate (English)

Currently translated at 100.0% (1501 of 1501 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/
2022-10-17 14:21:02 +02:00
Weblate 3549879e73 Translated using Weblate (French)
Currently translated at 100.0% (1502 of 1502 strings)

Translation: CryptPad/App
Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/
2022-10-17 14:21:02 +02:00
yflory bfd1b5138a Merge branch 'staging' into new-asciidoc 2022-10-17 14:07:02 +02:00
yflory d20b3ebee3 Merge branch 'staging' of github.com:xwiki-labs/cryptpad into staging 2022-10-17 14:02:53 +02:00
yflory 1838848ec8 Merge branch 'filter-doc-type' into staging 2022-10-17 14:02:40 +02:00
yflory 57d6f1e683 Merge branch 'staging' into code-app-fixes 2022-10-17 14:01:09 +02:00
yflory 0faa99fbfc Remove the drive filter on reload 2022-10-17 14:00:53 +02:00
yflory 8e05f159ae Merge branch 'staging' into filter-doc-type 2022-10-17 13:24:37 +02:00
David Benque bd82e86228 Remove empty file 2022-10-17 12:02:57 +02:00
David Benque 4f15fda226 Fix misaligned poll rows when swapping axes #975 2022-10-17 11:13:04 +02:00
yflory bde6bb0032 Clean server code 2022-10-14 16:53:38 +02:00
ansuz bdf8762fb6 fix typo that broke checkup page styles 2022-10-14 18:17:19 +05:30
David Benque 1ab0208f5b fix shrinking icons with long calendar titles 2022-10-12 17:20:29 +01:00
David Benque 81e00842b9 Style for default confirm button 2022-10-12 16:48:24 +01:00
David Benque 90a7b89a0c Use CryptPad blue for "all day" checkbox 2022-10-12 16:45:49 +01:00
yflory 8a3be878e8 Merge branch 'staging' into form-del 2022-10-12 17:19:22 +02:00
yflory c3df1bb0ec Use flag in pad metadata to allow line deletion in file 2022-10-12 17:17:58 +02:00
ansuz 2e4655e29c Merge branch 'soon' into absolute-paths 2022-10-12 10:55:43 +05:30
David Benque 0b87f1f9c6 Merge remote-tracking branch 'origin/rrule' into rrule 2022-10-11 16:02:10 +01:00
David Benque 5d96f94766 Align reminders line-height 2022-10-11 16:00:37 +01:00
yflory ce5609ea6a Add confirm step when deleting events 2022-10-11 17:00:13 +02:00
yflory f3dc8d059e Update weekly and monthly recurrence pickers in calendar 2022-10-11 16:43:37 +02:00
yflory 16cdf2ccdc Fix recurrence rule update issues 2022-10-11 16:11:53 +02:00
yflory c11b83dd5a Merge branch 'staging' into rrule 2022-10-11 14:00:30 +02:00
David Benque 99da655225 Adjust event popup style 2022-10-10 16:14:39 +01:00
David Benque 707ac44536 Improve notification alignment 2022-10-10 16:05:37 +01:00
David Benque 176f6ab594 Fix remove notification message 2022-10-10 16:05:15 +01:00
David Benque e568027d14 Fix button spacing in event popup 2022-10-10 16:04:23 +01:00
David Baker bf69a576bb
Make unparseable message non-fatal
There was code to handle unparseable messages from things like third party browser extensions but it caught a JSON parsing error, logged an error and then continued, which would throw an unhandled exception trying to read the `ack` property of `data` which had been left as `undefined`.

Add a `return` to abort execution of the handler function and ignore the message instead. Also changes the error to warning, since it's not a fatal error. Add a comment to clarify the behaviour.

From the existing comment, it looks like this was the original intention of the code, but the `return` was simply missed.
2022-10-07 15:57:18 +01:00
yflory 6a1c64fe9a Delete your own form answers 2022-10-06 17:12:23 +02:00
ansuz e59b1fc933 it's reasonable to expect Util.clone not to explode on undefined/null 2022-10-06 16:10:13 +05:30
ansuz 1acdb4180d fix for bogus metadata lines wiping ownership and other parameters 2022-10-06 16:05:05 +05:30
ansuz c03feef96e configure linter not to ignore the server. fix server linting issues 2022-10-06 15:34:58 +05:30
ansuz 863ab4f380 Merge branch 'soon' into absolute-paths 2022-10-06 15:34:05 +05:30
ansuz 3e3fc4f9e4 Merge branch 'main' into soon 2022-10-06 14:05:12 +05:30
ansuz 559e2d1e57 Merge branch 'staging' into soon 2022-10-06 13:06:45 +05:30
ansuz 01cdfa1bbc document yet another way that Safari/webkit is terrible 2022-10-05 15:17:07 +05:30
ansuz 5b4b68b31a [checkup] detect custom pages that might override critical functionality and warn against using CloudFlare 2022-10-05 15:08:49 +05:30
David Benque acfbdccf6f Update Readme
- Add forum as first point of contact
- Update screenshots with drive and full suite of applications
2022-10-03 16:13:10 +01:00
Mathilde f1490a2835
Fix broken links to forum tags
Calling out @davidbenque for messing this out. 😤
2022-10-03 16:51:39 +02:00
David Benque 699922fae6 Fix icon of Rich Text settings 2022-09-29 16:57:49 +01:00
Mathilde Blanchemanche 4eccf22c42 be specific about the type of reports allowed 2022-09-26 15:57:53 +02:00
Mathilde Blanchemanche c2b540793d get rid of FR only 2022-09-26 15:56:08 +02:00
Mathilde Blanchemanche 51d4a96ef8 Revert "remove bug report and feature request issue templates"
This reverts commit ef495142b0.
2022-09-26 15:55:42 +02:00
Mathilde Blanchemanche 34313ea048 be more specific for CryptPad.fr 2022-09-26 15:24:41 +02:00
Mathilde Blanchemanche 8d105d6f16 remove old entries and add news ones to the forum 2022-09-26 15:17:21 +02:00
Mathilde Blanchemanche ef495142b0 remove bug report and feature request issue templates 2022-09-26 15:09:16 +02:00
ansuz fede73efb1 enable admin option to opt-in to aggregate statistics 2022-09-22 16:35:20 +05:30
ansuz 9098af75ab fix spacing in loose lists in markdown 2022-09-22 15:54:56 +05:30
ansuz a8dcdbcbcb fix a bug which prevented old contacts from being removed 2022-09-22 14:20:27 +05:30
ansuz 112ad47bc6 guard against type errors when updating contacts UI based on contact data updates 2022-09-22 14:16:08 +05:30
ansuz 961e816cef avoid displaying empty strings as names when removing a contact 2022-09-22 14:15:27 +05:30
ansuz 174d97c442 test whether registration is open on public instances 2022-09-22 14:11:22 +05:30
yflory 89aabacc55 Merge branch 'rrule' of github.com:xwiki-labs/cryptpad into rrule 2022-09-21 11:10:09 +03:00
yflory b7024b23f5 Fix HTML encoding in calendar #736 2022-09-21 11:09:58 +03:00
David Benque 5c51fb3d84 Re-style notification section of calendar event modal 2022-09-19 17:26:53 +01:00
David Benque 29e5bf63dd Fix styles in calendar event modal 2022-09-19 17:26:14 +01:00
David Benque 34b5f66047 Add label for "remove reminder" 2022-09-19 17:25:15 +01:00
David Benque 31ca0c4518 Use primary button for day picker in custom recurring event settings 2022-09-19 17:23:59 +01:00
David Benque 7aaf1afeff Add title to custom recurring event End setting 2022-09-19 17:23:21 +01:00
yflory 57c3c28cb0 Support clickable links in calendar location 2022-09-15 17:19:12 +02:00
yflory c3b501b431 Fix incorrect recurrence with monthly events on day 31 2022-09-15 16:34:31 +02:00
yflory 175fb7e1dc Add button to move calendar to a specific date 2022-09-15 16:33:50 +02:00
yflory 6ddfd09805 Fix checkmark style in calendar app 2022-09-15 16:32:11 +02:00
yflory 65b00736bc Support recurrence rules when importing or exporting ICS calendars 2022-09-15 13:15:27 +02:00
yflory af923170a4 Merge branch 'rrule' of github.com:xwiki-labs/cryptpad into rrule 2022-09-14 17:42:06 +02:00
yflory 2e983bf52a Fix recurrence rule monthly picker 2022-09-14 17:40:44 +02:00
yflory 0ee48d40f2 Add click handlers to calendar reminders notifications 2022-09-14 16:11:54 +02:00
David Benque 57e801da98 Improve layout of calendar event pop-up 2022-09-14 13:52:50 +01:00
yflory f27d779aef Fix issues with reminders and recurrence rule 2022-09-13 16:43:08 +02:00
yflory ae32732483 Update recurrence rule 2022-09-13 14:18:38 +02:00
yflory 2dbefe864a lint compliance 2022-09-12 19:07:15 +02:00
yflory 0c273b1b3d Manage reminders for recurring events 2022-09-12 19:06:25 +02:00
yflory d35b42d207 Fix updating recurring events 2022-09-12 14:42:54 +02:00
yflory 3fa92fa155 Fix update original event 2022-09-08 17:03:08 +02:00
yflory 99df9bc21a Update recurring events in calendar 2022-09-08 16:56:58 +02:00
yflory ee5e1f8335 Translate recurrence rules into text 2022-09-07 17:08:41 +02:00
yflory c123434fa0 Fix recurrence issues 2022-09-07 12:56:45 +02:00
yflory 6ca6ecd283 Create recurring events 2022-09-06 18:58:39 +02:00
yflory 1d001f4ca4 Support inline mediatag in asciidoc rendering 2022-08-31 18:08:46 +02:00
Maxime Cesson 44e5d021ba Restrict pad creation to the current filtered type (when filter is active) 2022-08-31 16:22:51 +02:00
Maxime Cesson 13cba52778 Merge branch 'asciidoc' of github.com:xwiki-labs/cryptpad into asciidoc 2022-08-31 11:53:04 +02:00
Maxime Cesson 7906079a2b Render asciidoc syntax 2022-08-31 11:52:32 +02:00
yflory a5c4bc98ba Merge branch '5.1-candidate' into rrule 2022-08-31 11:43:38 +02:00
Maxime Cesson c4410f52d7 Improve filter button usability when a filter is active 2022-08-30 17:21:03 +02:00
yflory bdddb231af Display recurring events 2022-08-30 13:17:57 +02:00
Maxime Cesson febe51aabc Make drive toolbar responsive 2022-08-26 18:02:04 +02:00
Maxime Cesson 2c3f1f3b07 Small design improvements 2022-08-26 17:58:19 +02:00
Maxime Cesson e1c02d784e Rm useless filter stuff (from previous commit) 2022-08-26 17:51:39 +02:00
Maxime Cesson 581cb917b7 Render AsciiDoc preview 2022-08-23 12:00:44 +02:00
Maxime Cesson c2b3ed7ae7 Filter by document type (#438), to be improved 2022-08-12 12:25:06 +02:00
Maxime Cesson e382833545 C language suggested twice 2022-08-02 17:09:55 +02:00
Maxime Cesson caa4666e4a Jade programming language not handled, triggering warnings / errors 2022-08-02 17:07:21 +02:00
ansuz a93ab05310 handle absolute paths in a few obviously problematic cases 2021-11-22 18:16:35 +05:30
89 changed files with 8027 additions and 1538 deletions

View File

@ -1,5 +1,5 @@
name: Bug report
description: Report a reproducible bug for CryptPad. (NOT to be used for support questions.)
name: Reproducible bug report
description: Report a reproducible bug ONLY, otherwise see the links below
labels: ["bug"]
body:
- type: checkboxes

View File

@ -1,8 +1,17 @@
blank_issues_enabled: false
contact_links:
- name: CryptPad.fr issue
- name: Bug report
url: https://forum.cryptpad.org/t/bug-report
about: Use the appropriate tags & describe your issue in much details as possible
- name: Feature request
url: https://forum.cryptpad.org/t/feature-request
about: Check if an existing topic doesn't already cover the feature you'd like
- name: Generic question
url: https://forum.cryptpad.org/t/general
about: Feel free to use the forum to ask any question you might have
- name: CryptPad.fr flagship instance issue
url: https://cryptpad.fr/support/#new
about: Please use the integrated support form for CryptPad.fr
about: Issue with an account or document on cryptpad.fr only
- name: Report a security vulnerability
url: https://ouvaton.link/pOgHev
about: Please give us appropriate time to verify, respond and fix before disclosure

View File

@ -1,38 +0,0 @@
name: Feature Request
description: Suggest an idea for CryptPad.
labels: ["Feature Request"]
body:
- type: checkboxes
attributes:
label: Contribution guidelines
description: Please read the code of conduct before proceeding.
options:
- label: I've read the [code of conduct](https://github.com/xwiki-labs/cryptpad/blob/main/CODE_OF_CONDUCT.md) and wholeheartedly agree
required: true
- type: checkboxes
attributes:
label: I would like to request a new feature for CryptPad
description: Prior to creating a new issue, please check following:** *(fill out each checkbox with an `X` once done)*
options:
- label: I have searched past issues and labels to check that my question/idea does not exist
required: true
- label: I have understood that this report is dedicated for feature requests, and not for support-related inquiries.
required: true
- label: I have understood that answers are voluntary and community-driven, and not commercial support.
required: true
- type: textarea
attributes:
label: Summary
description: Please describe your idea in a reasonable amount of detail.
validations:
required: true
- type: textarea
attributes:
label: Motivation
description: Please describe how your idea would benefit you and other users.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request.

View File

@ -8,7 +8,6 @@ www/common/onlyoffice/v2*
www/common/onlyoffice/v4
www/common/onlyoffice/v5
server.js
www/scratch
www/accounts
www/lib

View File

@ -1,3 +1,96 @@
# 5.2.1
## Goals
This minor releases fixes a bug with one of the Form features introduced in 5.2.0.
We took the opportunity to include two other fixes for older issues.
## Bug Fixes
- The option to delete all responses to a form was not available to form authors when the form had been created in a drive (user or team) using the **+ NEW** button
- Drag & drop from a shared folder into the Templates folder made documents "disappear". They would reappear in the root of the drive when using a new worker (after all CryptPad tabs had been closed)
- Clicking a link in a Calendar event location field failed to open
## Update notes
Our `5.2.0` release introduced some changes to the Nginx configuration. If you are not already running `5.2.0` we recommend following the upgrade notes for that version first, and then updating to `5.2.1`
To do so:
1. Stop your server
2. Get the latest code with git
```bash
git fetch origin --tags
git checkout 5.2.1
```
1. Install the latest dependencies with `bower update`
2. Restart your server
3. Review your instance's checkup page to ensure that you are passing all tests
# 5.2.0
## Goals
This release is focused on addressing long-standing user feedback with new features. The most requested are improvements to Forms—multiple submissions and the ability to delete responses—as well as recurring events in Calendar.
## Features
- Forms
- New setting to allow participants (including Guests) to submit a form multiple times and/or delete their responses
- Notifications for form owners when new responses are submitted
- New option for form authors to delete all responses
- New option for form authors/auditors to export responses as JSON (in addition to existing CSV and CryptPad Sheet)
- Settings have been refactored in a modal with a summary in the main editor view
- Display fixes for long questions/options in some question types
- Calendar
- New event settings to repeat periodically
- quick default patterns (e.g. weekly on Mondays, yearly on December 14th, etc), and custom intervals
- modify one, future, or all events
- easily stop repetition from event preview
- Drive
- New button to filter the drive view by document type
- Teams
- Improved onboarding with the ability to use the same invitation link for a set number of people. Previously each link was limited to one use
- Initial role can now be set for invitation links, the recipient is assigned the role directly when joining, previously all new members joined as "Viewers"
- Code
- Asciidoc syntax support AND asciidoc rendering
- New jade language support
- Removed duplicate C-language option
- /checkup/
- [new test to confirm that public instances are open for registration](https://github.com/xwiki-labs/cryptpad/commit/174d97c442d5400d512dfccc478fd9fbd6fa075c)
- new test to check that the host provides an HSTS header
## Update notes
To update from `5.1.0` to `5.2.0`:
1. Read the **Nginx** section below to ensure you are using the right version and update your reverse proxy configuration to match the settings in our current `./docs/example.nginx.conf`
2. Reload nginx
3. Stop your API server
4. Fetch the latest code with git
5. Install the latest dependencies with `bower update` and `npm i`
6. Restart your server
7. Review your instance's checkup page to ensure that all tests are passing
### Nginx
We added some directives that may cause issues with older versions of Nginx. We now recommend and only support [Nginx stable](https://nginx.org/en/download.html). Please note that if you are running below `v1.14.2`, applying this update will likely result in breakage.
- Internet Protocol version 6 ([IPv6](https://en.wikipedia.org/wiki/IPv6)) support
- TLS generation, see [the recent tutorial](https://blog.cryptpad.org/2022/12/12/tutorial-nginx-tls-acme/) on our blog
- Better [TLS sessions](https://vincent.bernat.ch/en/blog/2011-ssl-session-reuse-rfc5077), handling timeout, tickets & longer cache
- Longer [HTTP Strict Transport Security](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) (HSTS), now 2 years
- [Online Certificate Status Protocol](https://en.wikipedia.org/wiki/OCSP_stapling) (OCSP) stapling support
# 5.1.0
## Goals

View File

@ -95,7 +95,7 @@ define([
return h('a', attrs, [icon, text]);
};
Pages.versionString = "5.1.0";
Pages.versionString = "5.2.1";
var customURLs = Pages.customURLs = {};
(function () {

View File

@ -183,6 +183,11 @@
}
}
}
.cp-dropdown-content {
a {
text-decoration: none;
}
}
}
.cp-alertify-type-container {
overflow: visible !important;

View File

@ -11,10 +11,12 @@
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background-color: @cp_scrollbar-fg;
width: 6px;
height: 6px;
}
height: 100%;
width: 100%;

View File

@ -120,6 +120,7 @@
border-width: 0 @checkmark-width @checkmark-width 0;
border-width: 0 var(--checkmark-width) var(--checkmark-width) 0;
position: absolute;
box-sizing: border-box;
}
&:focus {
box-shadow: 0px 0px 5px @cp_checkmark-back1;

View File

@ -48,7 +48,7 @@
button {
.fa-caret-down {
margin-right: 1em !important;
margin-right: 0.5em !important;
}
* {
.tools_unselectable();

View File

@ -91,7 +91,10 @@
height: 100%;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
background-color: @cp_buttons-primary;
background-color: @cp_buttons-default-color;
&.btn-primary, &.btn-secondary {
background-color: @cp_buttons-primary;
}
&.danger, &.btn-danger, &.danger-alt, &.btn-danger-alt {
background-color: @cp_buttons-red;
}
@ -327,6 +330,9 @@
fill: @cryptpad_text_col;
}
}
.flatpickr-monthDropdown-month {
background: @cp_flatpickr-bg;
}
}
.flatpickr-current-month {
span.cur-month:hover {

View File

@ -5,6 +5,7 @@
& {
each(@colortheme_apps, {
button .cp-icon-color-@{key},
.cp-icon-color-@{key} { color: @value; }
});

View File

@ -15,7 +15,14 @@
margin-bottom: 10px !important;
}
}
// todo ul, ol
// fix silly spacing around sublists in "loose lists"
:is(ul, ol) li > p {
& + :is(ul, ol) {
margin-bottom: 1rem;
}
}
// TOC
div.cp-md-toc {
background: @cp_markdown-bg;

View File

@ -123,12 +123,20 @@
display: flex;
align-items: center;
margin-bottom: 5px;
&.cp-teams-invite-role {
.cp-radio:not(:first-child) {
margin-left: 10px;
}
}
.cp-teams-help {
text-decoration: none !important;
}
}
.cp-teams-invite-message {
resize: none;
}
.cp-teams-invite-alert {
margin-top: 10px;
margin: 10px 0px 0px 0px !important;
}
.cp-teams-invite-spinner {
font-size: 1.2em;

View File

@ -27,14 +27,16 @@
color: @cryptpad_color_red;
}
}
.cp-avatar {
.avatar_main(30px);
padding: 0 5px;
.cp-reminder, .cp-avatar {
cursor: pointer;
&:hover {
background-color: @cp_dropdown-bg-hover;
}
}
.cp-avatar {
.avatar_main(30px);
padding: 0 5px;
}
.cp-notification-content {
flex: 1;
align-items: stretch;

View File

@ -967,6 +967,9 @@
}
}
}
.cp-toolbar-dropdown-nowrap {
white-space: nowrap;
}
.cp-toolbar-bottom {
color: @cp_toolbar-bottom-fg;
display: inline-flex;
@ -998,11 +1001,15 @@
.fa, .cptools {
margin-right: 5px;
}
.cp-dropdown-button-title .cp-icon {
margin-left: 5px;
}
&:hover {
background-color: fade(@cp_toolbar-bottom-bg, 70%);
}
}
.cp-toolbar-bottom-left > button,
.cp-toolbar-bottom-left > span > button,
.cp-toolbar-bottom-mid > button,
.cp-toolbar-bottom-right > button,
.cp-toolbar-bottom-right > span > button {
@ -1070,7 +1077,7 @@
.cp-toolbar-name, .cp-button-name {
display: none;
}
i {
i, span {
margin-right: 0;
}
}

View File

@ -6,6 +6,10 @@
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
# Let's Encrypt webroot
include letsencrypt-webroot;
# CryptPad serves static assets over these two domains.
# `main_domain` is what users will enter in their address bar.
@ -46,26 +50,35 @@ server {
# IMPORTANT: this config is intended to serve assets for at least two domains
# (your main domain and your sandbox domain). As such, you'll need to generate a single SSL certificate
# that includes both domains in order for things to work as expected.
ssl_certificate /home/cryptpad/.acme.sh/your-main-domain.com/fullchain.cer;
ssl_certificate_key /home/cryptpad/.acme.sh/your-main-domain.com/your-main-domain.com.key;
ssl_trusted_certificate /home/cryptpad/.acme.sh/your-main-domain.com/ca.cer;
ssl_certificate /etc/ssl/lets-encrypt/your-main-domain.com/cert;
ssl_certificate_key /etc/ssl/lets-encrypt/your-main-domain.com/key;
# diffie-hellman parameters are used to negotiate keys for your session
# generate strong parameters using the following command
ssl_dhparam /etc/nginx/dhparam.pem; # openssl dhparam -out /etc/nginx/dhparam.pem 4096
# Speeds things up a little bit when resuming a session
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:5m;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
# You'll need nginx 1.13.0 or better to support TLSv1.3
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# https://cipherli.st/
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# verify chain of trust of OCSP response using Root CA and Intermediate certs
ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
# replace with the IP address of your resolver
resolver 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 9.9.9.9 149.112.112.112 208.67.222.222 208.67.220.220;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
add_header Access-Control-Allow-Origin "${allowed_origins}";
@ -84,12 +97,17 @@ server {
error_page 404 /customize.dist/404.html;
# any static assets loaded with "ver=" in their URL will be cached for a year
if ($uri ~ ^(\/|.*\/|.*\.html)$) {
set $cacheControl no-cache;
}
if ($args ~ ver=) {
set $cacheControl max-age=31536000;
}
# This rule overrides the above caching directive and makes things somewhat less efficient.
# We had inverted them as an optimization, but Safari 16 introduced a bug that interpreted
# some important headers incorrectly when loading these files from cache.
# This is why we can't have nice things :(
if ($uri ~ ^(\/|.*\/|.*\.html)$) {
set $cacheControl no-cache;
}
# Will not set any header if it is emptystring
add_header Cache-Control $cacheControl;

View File

@ -6,6 +6,7 @@ const nThen = require("nthen");
const Core = require("./core");
const Metadata = require("./metadata");
const HK = require("../hk-util");
const Nacl = require("tweetnacl/nacl-fast");
Channel.disconnectChannelMembers = function (Env, Server, channelId, code, cb) {
var done = Util.once(Util.mkAsync(cb));
@ -207,6 +208,40 @@ Channel.trimHistory = function (Env, safeKey, data, cb) {
});
};
// Delete a signed mailbox message. This is used when users want
// to delete their form reponses.
Channel.deleteMailboxMessage = function (Env, data, cb) {
const channelId = data.channel;
const hash = data.hash;
const proof = data.proof;
let nonce, proofBytes;
try {
nonce = Nacl.util.decodeBase64(proof.split('|')[0]);
proofBytes = Nacl.util.decodeBase64(proof.split('|')[1]);
} catch (e) {
return void cb('EINVAL');
}
Env.msgStore.deleteChannelLine(channelId, hash, function (msg) {
// Check if you're allowed to delete this hash
try {
const mySecret = new Uint8Array(32);
const msgBytes = Nacl.util.decodeBase64(msg).subarray(64); // Remove signature
const theirPublic = msgBytes.subarray(24,56); // 0-24 = nonce; 24-56=publickey (32 bytes)
const hashBytes = Nacl.box.open(proofBytes, nonce, theirPublic, mySecret);
return Nacl.util.encodeUTF8(hashBytes) === hash;
} catch (e) {
return false;
}
}, function (err) {
if (err) { return void cb(err); }
// clear historyKeeper's cache for this channel
Env.historyKeeper.channelClose(channelId);
cb();
delete Env.channel_cache[channelId];
delete Env.metadata_cache[channelId];
});
};
var ARRAY_LINE = /^\[/;
/* Files can contain metadata but not content
@ -320,10 +355,11 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) {
Server.getChannelUserList(channelId).forEach(function (userId) {
Server.send(userId, fullMessage);
});
cb();
});
cb();
});
};

View File

@ -11,6 +11,7 @@ const Core = require("./commands/core");
const Quota = require("./commands/quota");
const Util = require("./common-util");
const Package = require("../package.json");
const Path = require("path");
var canonicalizeOrigin = function (s) {
if (typeof(s) === 'undefined') { return; }
@ -296,7 +297,7 @@ module.exports.create = function (config) {
var paths = Env.paths;
var keyOrDefaultString = function (key, def) {
return typeof(config[key]) === 'string'? config[key]: def;
return Path.resolve(typeof(config[key]) === 'string'? config[key]: def);
};
Env.incrementBytesWritten = function (n) {

View File

@ -31,7 +31,8 @@ the owners field is guaranteed to exist.
* mailbox <STRING|MAP>
* ADD_MAILBOX
* RM_MAILBOX
* deleteLines <BOOLEAN>
* ALLOW_LINE_DELETION
*/
var commands = {};
@ -71,6 +72,24 @@ commands.RESTRICT_ACCESS = function (meta, args) {
return true;
};
// ["ALLOW_LINE_DELETION", [true], 1561623438989]
// ["ALLOW_LINE_DELETION", [false], 1561623438989]
commands.ALLOW_LINE_DELETION = function (meta, args) {
if (!Array.isArray(args) || typeof(args[0]) !== 'boolean') {
throw new Error('INVALID_STATE');
}
var bool = args[0];
// reject the proposed command if there is no change in state
if (meta.deleteLines === bool) { return false; }
// apply the new state
meta.deleteLines = args[0];
return true;
};
// ["ADD_ALLOWED", ["7eEqelGso3EBr5jHlei6av4r9w2B9XZiGGwA1EgZ-5I=", ...], 1561623438989]
commands.ADD_ALLOWED = function (meta, args) {
if (!Array.isArray(args)) {
@ -386,6 +405,7 @@ Meta.createLineHandler = function (ref, errorHandler) {
ref.meta = {};
ref.index = 0;
ref.logged = {};
var overwritten = false;
return function (err, line) {
if (err) {
@ -430,6 +450,8 @@ Meta.createLineHandler = function (ref, errorHandler) {
// Thus, accept both the first and second lines you process as valid initial state
// preferring the second if it exists
if (index < 2 && line && typeof(line) === 'object') {
if (overwritten) { return; } // hack to avoid overwriting metadata a second time
overwritten = true;
// special case!
ref.meta = line;
return;

View File

@ -20,6 +20,7 @@ const UNAUTHENTICATED_CALLS = {
IS_CHANNEL_PINNED: Pinning.isChannelPinned, // FIXME drop this RPC
IS_NEW_CHANNEL: Channel.isNewChannel,
WRITE_PRIVATE_MESSAGE: Channel.writePrivateMessage,
DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage,
GET_METADATA: Metadata.getMetadata,
};

View File

@ -73,9 +73,11 @@ Stats.instanceData = function (Env) {
//data.archiveRetentionTime = Env.archiveRetentionTime,
}
// we won't consider instances for public listings
// unless they opt to provide more info about themselves
if (!Env.provideAggregateStatistics) { return data; }
// Admins can opt-in to providing more detailed information about the extent of the instance's usage
if (!Env.provideAggregateStatistics) {
// check how many instances provide stats before we put more work into it
data.providesAggregateStatistics = true;
}
return data;
};

View File

@ -928,7 +928,7 @@ var getMessages = function (env, chanName, handler, cb) {
});
};
var trimChannel = function (env, channelName, hash, _cb) {
var filterMessages = function (env, channelName, check, filterHandler, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
// this function is queued as a blocking action for the relevant channel
@ -985,6 +985,13 @@ var trimChannel = function (env, channelName, hash, _cb) {
}
// if there were no errors just fall through to the next block
}));
}).nThen(function (w) {
// If we want to delete a signle line of the file, make sure this pad allows it
if (typeof(check) !== "function") { return; }
if (!metadataReference.meta || !check(metadataReference.meta)) {
w.abort();
return void cb("EFORBIDDEN");
}
}).nThen(function (w) {
// create temp buffer writeStream
tempStream = Fs.createWriteStream(tempChannelPath, {
@ -1023,14 +1030,15 @@ var trimChannel = function (env, channelName, hash, _cb) {
if (!msg) { return void readMore(); }
var msgHash = Extras.getHash(msg[4]);
if (msgHash === hash) {
// everything from this point on should be retained
retain = true;
return void tempStream.write(s_msg + '\n', function () {
var remove = function () { readMore(); };
var preserve = function () {
tempStream.write(s_msg + '\n', function () {
readMore();
});
}
readMore();
};
var preserveRemaining = function () { retain = true; };
filterHandler(msg, msgHash, abort, remove, preserve, preserveRemaining);
};
readMessagesBin(env, channelName, 0, handler, w(function (err) {
@ -1102,13 +1110,42 @@ var trimChannel = function (env, channelName, hash, _cb) {
});
});
};
var deleteChannelLine = function (env, channelName, hash, checkRights, _cb) {
var check = function (meta) { return Boolean(meta.deleteLines); };
var handler = function (msg, msgHash, abort, remove, preserve, preserveRemaining) {
if (msgHash === hash) {
if (typeof(checkRights) === "function" && !checkRights(msg[4])) {
// Not allowed: abort
return void abort();
}
// Line found: remove it and preserve all remaining lines
preserveRemaining();
return void remove();
}
// Continue until we find the correct hash
preserve();
};
filterMessages(env, channelName, check, handler, _cb);
};
var trimChannel = function (env, channelName, hash, _cb) {
var handler = function (msg, msgHash, abort, remove, preserve, preserveRemaining) {
if (msgHash === hash) {
// Everything from this point on should be retained
preserveRemaining();
return void preserve();
}
// Remove until we find our hash
remove();
};
filterMessages(env, channelName, null, handler, _cb);
};
module.exports.create = function (conf, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var env = {
root: conf.filePath || './datastore',
archiveRoot: conf.archivePath || './data/archive',
root: Path.resolve(conf.filePath || './datastore'),
archiveRoot: Path.resolve(conf.archivePath || './data/archive'),
// supply a volumeId if you want a store to archive channels to and from
// to its own subpath within the archive directory
volumeId: conf.volumeId || 'datastore',
@ -1236,6 +1273,12 @@ module.exports.create = function (conf, _cb) {
trimChannel(env, channelName, hash, Util.both(cb, next));
});
},
deleteChannelLine: function (channelName, hash, checkRights, cb) {
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
schedule.blocking(channelName, function (next) {
deleteChannelLine(env, channelName, hash, checkRights, Util.both(cb, next));
});
},
// check if a channel exists in the database
isChannelAvailable: function (channelName, cb) {

734
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "5.1.0",
"version": "5.2.1",
"license": "AGPL-3.0+",
"repository": {
"type": "git",
@ -15,7 +15,7 @@
"@mcrowe/minibloom": "^0.2.0",
"chainpad-crypto": "^0.2.5",
"chainpad-server": "^5.1.0",
"express": "~4.16.0",
"express": "~4.18.2",
"fs-extra": "^7.0.0",
"get-folder-size": "^2.0.1",
"netflux-websocket": "^0.1.20",

View File

@ -2,7 +2,9 @@
CryptPad is a collaboration suite that is end-to-end-encrypted and open-source. It is built to enable collaboration, synchronizing changes to documents in real time. Because all data is encrypted, the service and its administrators have no way of seeing the content being edited and stored.
![CryptPad screenshot](screenshot.png "Private real-time collaboration on a Rich Text document.")
![Drive screenshot](screenshot.png "preview of the CryptDrive")
![Suite screenshots](screenshot-suite.png "all CyptPad applications: Document, Sheet, Presentation, Form, Kanban, Code, Rich Text, Whiteboard")
# Installation
@ -62,10 +64,12 @@ More information about this can be found in [our translation guide](/customize.d
# Contacting Us
You can reach members of the CryptPad development team on [Twitter](https://twitter.com/cryptpad),
via our [GitHub issue tracker](https://github.com/xwiki-labs/cryptpad/issues/), on our
[Matrix channel](https://riot.im/app/#/room/#cryptpad:matrix.org), or by
[e-mail](mailto:research@xwiki.com).
The best places to reach the development team and the community are the [CryptPad Forum](https://forum.cryptpad.org) and the [Matrix chat](https://matrix.to/#/#cryptpad:matrix.xwiki.com)
The team is also on social media:
- Mastodon: [@cryptpad@fosstodon.org](https://fosstodon.org/@cryptpad)
- Twitter: [@cryptpad](https://twitter.com/cryptpad)
# Team

BIN
screenshot-suite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 125 KiB

View File

@ -1,5 +1,5 @@
/*
globals require console
globals process
*/
var Express = require('express');
var Http = require('http');
@ -8,7 +8,6 @@ var Path = require("path");
var nThen = require("nthen");
var Util = require("./lib/common-util");
var Default = require("./lib/defaults");
var Keys = require("./lib/keys");
var config = require("./lib/load-config");
var Env = require("./lib/env").create(config);
@ -116,16 +115,17 @@ app.head(/^\/common\/feedback\.html/, function (req, res, next) {
});
}());
const serveStatic = Express.static(Env.paths.blob, {
setHeaders: function (res) {
res.set('Access-Control-Allow-Origin', Env.enableEmbedding? '*': Env.permittedEmbedders);
res.set('Access-Control-Allow-Headers', 'Content-Length');
res.set('Access-Control-Expose-Headers', 'Content-Length');
}
});
app.use('/blob', function (req, res, next) {
if (req.method === 'HEAD') {
Express.static(Path.join(__dirname, Env.paths.blob), {
setHeaders: function (res, path, stat) {
res.set('Access-Control-Allow-Origin', Env.enableEmbedding? '*': Env.permittedEmbedders);
res.set('Access-Control-Allow-Headers', 'Content-Length');
res.set('Access-Control-Expose-Headers', 'Content-Length');
}
})(req, res, next);
return;
return void serveStatic(req, res, next);
}
next();
});
@ -150,32 +150,33 @@ app.use(function (req, res, next) {
// serve custom app content from the customize directory
// useful for testing pages customized with opengraph data
app.use(Express.static(__dirname + '/customize/www'));
app.use(Express.static(__dirname + '/www'));
app.use(Express.static(Path.resolve('customize/www')));
app.use(Express.static(Path.resolve('www')));
// FIXME I think this is a regression caused by a recent PR
// correct this hack without breaking the contributor's intended behaviour.
var mainPages = config.mainPages || Default.mainPages();
var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$');
app.get(mainPagePattern, Express.static(__dirname + '/customize'));
app.get(mainPagePattern, Express.static(__dirname + '/customize.dist'));
app.get(mainPagePattern, Express.static(Path.resolve('customize')));
app.get(mainPagePattern, Express.static(Path.resolve('customize.dist')));
app.use("/blob", Express.static(Path.join(__dirname, Env.paths.blob), {
app.use("/blob", Express.static(Env.paths.blob, {
maxAge: Env.DEV_MODE? "0d": "365d"
}));
app.use("/datastore", Express.static(Path.join(__dirname, Env.paths.data), {
app.use("/datastore", Express.static(Env.paths.data, {
maxAge: "0d"
}));
app.use("/block", Express.static(Path.join(__dirname, Env.paths.block), {
app.use("/block", Express.static(Env.paths.block, {
maxAge: "0d",
}));
app.use("/customize", Express.static(__dirname + '/customize'));
app.use("/customize", Express.static(__dirname + '/customize.dist'));
app.use("/customize.dist", Express.static(__dirname + '/customize.dist'));
app.use(/^\/[^\/]*$/, Express.static('customize'));
app.use(/^\/[^\/]*$/, Express.static('customize.dist'));
app.use("/customize", Express.static(Path.resolve('customize')));
app.use("/customize", Express.static(Path.resolve('customize.dist')));
app.use("/customize.dist", Express.static(Path.resolve('customize.dist')));
app.use(/^\/[^\/]*$/, Express.static(Path.resolve('customize')));
app.use(/^\/[^\/]*$/, Express.static(Path.resolve('customize.dist')));
// if dev mode: never cache
var cacheString = function () {
@ -216,7 +217,7 @@ var makeRouteCache = function (template, cacheName) {
};
};
var serveConfig = makeRouteCache(function (host) {
var serveConfig = makeRouteCache(function () {
return [
'define(function(){',
'return ' + JSON.stringify({
@ -244,10 +245,10 @@ var serveConfig = makeRouteCache(function (host) {
accounts_api: Env.accounts_api,
}, null, '\t'),
'});'
].join(';\n')
].join(';\n');
}, 'configCache');
var serveBroadcast = makeRouteCache(function (host) {
var serveBroadcast = makeRouteCache(function () {
var maintenance = Env.maintenance;
if (maintenance && maintenance.end && maintenance.end < (+new Date())) {
maintenance = undefined;
@ -260,21 +261,21 @@ var serveBroadcast = makeRouteCache(function (host) {
maintenance: maintenance
}, null, '\t'),
'});'
].join(';\n')
].join(';\n');
}, 'broadcastCache');
app.get('/api/config', serveConfig);
app.get('/api/broadcast', serveBroadcast);
var define = function (obj) {
var defineBlock = function (obj) {
return `define(function (){
return ${JSON.stringify(obj, null, '\t')};
});`
});`;
};
app.get('/api/instance', function (req, res) { // XXX use caching?
res.setHeader('Content-Type', 'text/javascript');
res.send(define({
res.send(defineBlock({
name: Env.instanceName,
description: Env.instanceDescription,
location: Env.instanceJurisdiction,
@ -282,10 +283,10 @@ app.get('/api/instance', function (req, res) { // XXX use caching?
}));
});
var four04_path = Path.resolve(__dirname + '/customize.dist/404.html');
var fivehundred_path = Path.resolve(__dirname + '/customize.dist/500.html');
var custom_four04_path = Path.resolve(__dirname + '/customize/404.html');
var custom_fivehundred_path = Path.resolve(__dirname + '/customize/500.html');
var four04_path = Path.resolve('customize.dist/404.html');
var fivehundred_path = Path.resolve('customize.dist/500.html');
var custom_four04_path = Path.resolve('customize/404.html');
var custom_fivehundred_path = Path.resolve('/customize/500.html');
var send404 = function (res, path) {
if (!path && path !== four04_path) { path = four04_path; }
@ -321,7 +322,7 @@ app.get('/api/updatequota', function (req, res) {
});
});
app.get('/api/profiling', function (req, res, next) {
app.get('/api/profiling', function (req, res) {
if (!Env.enableProfiling) { return void send404(res); }
res.setHeader('Content-Type', 'text/javascript');
res.send(JSON.stringify({
@ -329,13 +330,13 @@ app.get('/api/profiling', function (req, res, next) {
}));
});
app.use(function (req, res, next) {
app.use(function (req, res) {
res.status(404);
send404(res, custom_four04_path);
});
// default message for thrown errors in ExpressJS routes
app.use(function (err, req, res, next) {
app.use(function (err, req, res) {
Env.Log.error('EXPRESSJS_ROUTING', {
error: err.stack || err,
});
@ -346,7 +347,7 @@ app.use(function (err, req, res, next) {
var httpServer = Env.httpServer = Http.createServer(app);
nThen(function (w) {
Fs.exists(__dirname + "/customize", w(function (e) {
Fs.exists(Path.resolve("customize"), w(function (e) {
if (e) { return; }
console.log("CryptPad is customizable, see customize.dist/readme.md for details");
}));
@ -377,7 +378,7 @@ nThen(function (w) {
Http.createServer(app).listen(Env.httpSafePort, Env.httpAddress, w());
}
}).nThen(function () {
var wsConfig = { server: httpServer };
//var wsConfig = { server: httpServer };
// Initialize logging then start the API server
require("./lib/log").create(config, function (_log) {

View File

View File

@ -105,7 +105,7 @@ define([
'cp-admin-update-available',
'cp-admin-checkup',
'cp-admin-block-daily-check',
//'cp-admin-provide-aggregate-statistics',
'cp-admin-provide-aggregate-statistics',
'cp-admin-list-my-instance',
'cp-admin-consent-to-contact',

View File

@ -17,6 +17,9 @@
.cp-small { display: none; }
}
.flatpickr-calendar {
z-index: 100001 !important; // Alertify is 100000
}
#cp-sidebarlayout-container #cp-sidebarlayout-rightside {
padding: 0;
& > div {
@ -101,6 +104,21 @@
color: @cryptpad_text_col !important;
}
}
.tui-full-calendar-floating-layer.cp-calendar-popup-flex {
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
justify-content: center !important;
align-items: center !important;
.tui-full-calendar-popup {
width: 540px !important;
}
}
#tui-full-calendar-popup-arrow {
display: none !important;
}
}
.tui-full-calendar-timegrid-timezone {
background-color: @cp_sidebar-right-bg !important;
@ -116,13 +134,30 @@
border-color: @cp_calendar-border !important;
}
.tui-full-calendar-popup {
border-radius: @variables_radius_L;
}
.tui-full-calendar-popup-container {
background: @cp_flatpickr-bg;
color: @cryptpad_text_col;
border-radius: @variables_radius;
border-radius: @variables_radius_L;
font-weight: normal;
.tui-full-calendar-icon:not(.tui-full-calendar-calendar-dot):not(.tui-full-calendar-dropdown-arrow):not(.tui-full-calendar-ic-checkbox) {
display: none;
}
.tui-full-calendar-popup-detail-item {
a {
color: @cryptpad_color_link;
text-decoration: underline;
}
}
.tui-full-calendar-section-button-save {
height: 40px;
.btn-primary { // Update button
margin-right: 0px;
}
}
}
li.tui-full-calendar-popup-section-item {
padding: 0 6px;
@ -186,6 +221,9 @@
width: 100%;
height: 32px;
border-radius: @variables_radius;
input[type="checkbox"].tui-full-calendar-checkbox-square:checked + span {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABbmlDQ1BpY2MAACiRdZHNK0RRGMZ/Zoh8NIqFNItZDFmMEiVLxsJmkgZlsLn3zpe6c93uvZNkq2wspizExtfCf8BW2VJKkZKs/AG+Npqu97hqJM7t3PfXc87zds5zIJQyjZJbPwAly3PSE8nYXGY+1vhEMy20EyaqGa49NjWV4t/xfkOdqtf9qtf/+/4cLdmca0Bdk/CwYTue8KhwasWzFW8KdxpFLSu8L5xw5IDCF0rXA35UXAj4VbEzkx6HkOoZK/xg/QcbRack3CccL5ll4/s86iatOWt2Wmq3zCguaSZIEkOnzBImHv1SLcnsb9/Al2+SZfEY8rdZxRFHgaJ4E6KWpWtOal70nHwmqyr333m6+aHBoHtrEhoefP+lBxq3oFrx/Y8D368eQvgezqyaf1lyGnkTvVLT4nsQWYeT85qmb8PpBnTd2ZqjfUlhmaF8Hp6PoS0DHVfQvBBk9b3O0S3MrMkTXcLOLvTK/sjiJ6CLZ94KREMsAAAACXBIWXMAAAsSAAALEgHS3X78AAABHUlEQVQoFWNkaP//n4EMwESGHrAW6mtkZvz3G2g03BsBagz/nRUQ7sNp49//TJ+Byv6BlMbrMjCsC2Jg3BbKwOAgB9GMolGAg4GBESIOIrmA+A9I03xvFHGwCrhGkDOe5TAw9LvAFbEn6jEwwTT9BtodvJ7h/4FHYH0MLBCKgaHTgYGBE8jLN2FgYAJae+4FA8NcLwZWkAtAmoLWMTBsuYNwECMsHrVEGBj2RzEwiIEciATANgE1bb6DJAgMNLhTr71hYHBcxsDw+htCAQ5NIAU/4RpBPKjmf2++MfwHaQpZD7cJyEMB3+BORRL+rybE8FOEk4Hj2FO4KMgdrFAMitun2DTCVSMxYAkBFFYg9j94qCIpwsZEil5wyDIDAAXIUsnSKmq7AAAAAElFTkSuQmCC);
}
.tui-full-calendar-ic-checkbox {
margin-left: 5px;
border-radius: 2px;
@ -196,6 +234,7 @@
.tui-full-calendar-popup-detail {
font: @colortheme_app-font;
color: @cryptpad_text_col;
box-shadow: @cryptpad_ui_shadow;
.tui-full-calendar-popup-container {
padding-bottom: 17px;
}
@ -203,28 +242,44 @@
font-size: 14px;
}
.tui-full-calendar-section-button {
margin-top: 10px;
border: 0;
display: flex;
align-items: center;
align-items: start;
button {
flex: 1;
margin: 0;
}
}
.tui-full-calendar-popup-top-line {
border-radius: 10px 10px 0px 0px;
height: 10px;
}
.tui-full-calendar-popup-vertical-line {
visibility: hidden;
width: 10px;
}
}
.cp-recurrence-label, .cp-notif-label {
color: @cryptpad_text_col;
margin-right: 1rem;
i {
margin-right: 0.5rem;
}
}
.cp-calendar-recurrence-container {
margin-top: 1rem;
.cp-calendar-rec-translated-str {
margin-top: 0.5rem;
}
}
.cp-calendar-add-notif {
flex-flow: column;
align-items: baseline !important;
margin: 10px 0;
.cp-notif-label {
color: @cp_sidebar-hint;
margin-right: 20px;
}
margin: 1rem 0;
* {
font-size: @colortheme_app-font-size;
font-weight: normal;
@ -234,34 +289,58 @@
}
.cp-calendar-notif-list-container {
margin-bottom: 10px;
.cp-notif-label {
margin-top: 0.5em;
}
}
.cp-calendar-notif-list {
display: flex;
flex-flow: column;
.cp-notif-entry {
margin-bottom: 2px;
border-radius: @variables_radius;
background-color: fade(@cryptpad_text_col, 10%);
padding: 0.25rem;
.cp-notif-value {
width: 170px;
display: inline-flex;
line-height: 30px;
vertical-align: middle;
.cp-before {
flex: 1;
min-width: 0;
}
}
span:not(:last-child) {
margin-right: 5px;
margin: 0px 5px;
}
.btn-danger-outline {
margin-right: 0px !important;
background-color: transparent;
color: @cryptpad_text_col;
border-color: @cryptpad_text_col;
&:hover {
color: @cp_buttons-red-color;
background-color: @cp_buttons-red;
border-color: @cp_buttons-red;
}
}
}
}
.cp-notif-empty {
display: none;
margin-bottom: 2px;
border-radius: @variables_radius;
background-color: fade(@cryptpad_text_col, 10%);
padding: 0.25rem 0.5rem;
line-height: 30px;
}
.cp-calendar-notif-list:empty ~ .cp-notif-empty {
display: block;
}
.cp-calendar-notif-form {
align-items: center;
margin-bottom: 20px;
// margin-bottom: 20px;
input {
width: 80px;
margin-right: 5px;
@ -270,15 +349,120 @@
}
.cp-calendar-close {
top: 17px;
right: 17px;
height: auto;
margin-right: 0px;
line-height: initial;
border: 1px solid;
&:not(:hover) {
background: transparent;
}
}
}
.cp-calendar-rec-inline, .cp-calendar-rec-block {
&:not(:last-child) {
margin-bottom: 10px;
}
}
.cp-calendar-rec-inline {
display: flex;
flex-flow: row;
align-items: center;
& > *:not(:first-child) { margin-left: 5px; }
.cp-dropdown-container {
position: unset;
}
input[type="number"] {
width: 80px !important;
margin-bottom: 0 !important;
}
.cp-checkmark {
margin-right: 0.5rem;
}
}
.cp-calendar-rec-block {
.cp-calendar-rec-block-title {
margin-bottom: 0.5rem !important;
}
.cp-radio {
margin-bottom: 0.5rem;
}
input[type="radio"]:not(:checked) ~ .cp-checkmark-label {
input {
filter: grayscale(1);
}
}
.cp-checkmark-label {
& > *:not(:first-child) { margin-left: 5px; }
width: 100%;
//height: 26px;
display: flex;
align-items: center;
& > input {
margin-bottom: 0 !important;
}
input {
display: inline;
height: 24px !important;
padding: 0 5px !important;
}
input[type="text"] {
width: 200px !important;
}
input[type="number"] {
width: 80px !important;
margin-bottom: 0 !important;
}
}
}
#cp-calendar-rec-monthly-pick ~ .cp-checkmark-label {
display: flex;
align-items: center;
& > span {
margin-right: 20px;
}
}
button.cp-calendar-pick-el {
display: flex;
align-items: center;
justify-content: center;
&:not(:last-child) {
margin-right: 5px;
}
}
div.cp-calendar-weekly-pick {
button {
width: 50px;
}
}
div.cp-calendar-monthly-pick {
display: flex;
flex-flow: column;
& > div {
display: flex;
&:not(:last-child) {
margin-bottom: 5px;
}
button {
height: 25px;
width: 25px;
&.lastday {
width: 115px;
}
}
}
}
.tui-full-calendar-ic-repeat-b {
display: none;
& ~ * {
display: none;
}
}
#cp-toolbar .cp-calendar-browse {
display: flex;
align-items: center;
@ -395,6 +579,7 @@
align-items: center;
justify-content: center;
border-radius: @variables_radius;
flex-shrink: 0;
}
&.cp-active {
background-color: @cp_sidebar-left-item-bg;

View File

@ -2,7 +2,9 @@
// Calendars will be exported using this format instead of plain text.
define([
'/customize/pages.js',
], function (Pages) {
'/common/common-util.js',
'/calendar/recurrence.js'
], function (Pages, Util, Rec) {
var module = {};
var getICSDate = function (str) {
@ -57,60 +59,197 @@ define([
var data = content[uid];
// DTSTAMP: now...
// UID: uid
var start, end;
if (data.isAllDay && data.startDay && data.endDay) {
start = "DTSTART;VALUE=DATE:" + getDate(data.startDay);
end = "DTEND;VALUE=DATE:" + getDate(data.endDay, true);
} else {
start = "DTSTART:"+getICSDate(data.start);
end = "DTEND:"+getICSDate(data.end);
}
var getDT = function (data) {
var start, end;
if (data.isAllDay) {
var startDate = new Date(data.start);
var endDate = new Date(data.end);
data.startDay = data.startDay || (startDate.getFullYear() + '-' + (startDate.getMonth()+1) + '-' + startDate.getDate());
data.endDay = data.endDay || (endDate.getFullYear() + '-' + (endDate.getMonth()+1) + '-' + endDate.getDate());
start = "DTSTART;VALUE=DATE:" + getDate(data.startDay);
end = "DTEND;VALUE=DATE:" + getDate(data.endDay, true);
} else {
start = "DTSTART:"+getICSDate(data.start);
end = "DTEND:"+getICSDate(data.end);
}
return {
start: start,
end: end
};
};
Array.prototype.push.apply(ICS, [
'BEGIN:VEVENT',
'DTSTAMP:'+getICSDate(+new Date()),
'UID:'+uid,
start,
end,
'SUMMARY:'+ data.title,
'LOCATION:'+ data.location,
]);
if (Array.isArray(data.reminders)) {
data.reminders.forEach(function (valueMin) {
var time = valueMin * 60;
var days = Math.floor(time / DAY);
time -= days * DAY;
var hours = Math.floor(time / HOUR);
time -= hours * HOUR;
var minutes = Math.floor(time / MINUTE);
time -= minutes * MINUTE;
var seconds = time;
var str = "-P" + days + "D";
if (hours || minutes || seconds) {
str += "T" + hours + "H" + minutes + "M" + seconds + "S";
var getRRule = function (data) {
if (!data.recurrenceRule || !data.recurrenceRule.freq) { return; }
var r = data.recurrenceRule;
var rrule = "RRULE:";
rrule += "FREQ="+r.freq.toUpperCase();
Object.keys(r).forEach(function (k) {
if (k === "freq") { return; }
if (k === "by") {
Object.keys(r.by).forEach(function (_k) {
rrule += ";BY"+_k.toUpperCase()+"="+r.by[_k];
});
return;
}
rrule += ";"+k.toUpperCase()+"="+r[k];
});
return rrule;
};
var addEvent = function (arr, data, recId) {
var uid = data.id;
var dt = getDT(data);
var start = dt.start;
var end = dt.end;
var rrule = getRRule(data);
Array.prototype.push.apply(arr, [
'BEGIN:VEVENT',
'DTSTAMP:'+getICSDate(+new Date()),
'UID:'+uid,
start,
end,
recId,
rrule,
'SUMMARY:'+ data.title,
'LOCATION:'+ data.location,
].filter(Boolean));
if (Array.isArray(data.reminders)) {
data.reminders.forEach(function (valueMin) {
var time = valueMin * 60;
var days = Math.floor(time / DAY);
time -= days * DAY;
var hours = Math.floor(time / HOUR);
time -= hours * HOUR;
var minutes = Math.floor(time / MINUTE);
time -= minutes * MINUTE;
var seconds = time;
var str = "-P" + days + "D";
if (hours || minutes || seconds) {
str += "T" + hours + "H" + minutes + "M" + seconds + "S";
}
Array.prototype.push.apply(arr, [
'BEGIN:VALARM',
'ACTION:DISPLAY',
'DESCRIPTION:This is an event reminder',
'TRIGGER:'+str,
'END:VALARM'
]);
});
}
if (Array.isArray(data.cp_hidden)) {
Array.prototype.push.apply(arr, data.cp_hidden);
}
arr.push('END:VEVENT');
};
var applyChanges = function (base, changes) {
var applyDiff = function (obj, k) {
var diff = obj[k]; // Diff is always compared to origin start/end
var d = new Date(base[k]);
d.setDate(d.getDate() + diff.d);
d.setHours(d.getHours() + diff.h);
d.setMinutes(d.getMinutes() + diff.m);
base[k] = +d;
};
Object.keys(changes || {}).forEach(function (k) {
if (k === "start" || k === "end") {
return applyDiff(changes, k);
}
base[k] = changes[k];
});
};
var prev = data;
// Check if we have "one-time" or "from date" updates.
// "One-time" updates will be added accordingly to the ICS specs
// "From date" updates will be added as new events and will add
// an "until" value to the initial event's RRULE
var toAdd = [];
if (data.recurrenceRule && data.recurrenceRule.freq && data.recUpdate) {
var ru = data.recUpdate;
var _all = {};
var duration = data.end - data.start;
var all = Rec.getAllOccurrences(data); // "false" if infinite
Object.keys(ru.from || {}).forEach(function (d) {
if (!Object.keys(ru.from[d] || {}).length) { return; }
_all[d] = _all[d] || {};
_all[d].from = ru.from[d];
});
Object.keys(ru.one || {}).forEach(function (d) {
if (!Object.keys(ru.one[d] || {}).length) { return; }
_all[d] = _all[d] || {};
_all[d].one = ru.one[d];
});
Object.keys(_all).sort(function (a, b) {
return Number(a) - Number(b);
}).forEach(function (d) {
d = Number(d);
var r = prev.recurrenceRule;
// This rule won't apply if we've reached "until" or "count"
var idx = all && all.indexOf(d);
if (all && idx === -1) {
// Make sure we don't have both count and until
if (all.length === r.count) { delete r.until; }
else { delete r.count; }
return;
}
var ud = _all[d];
if (ud.from) { // "From" updates are not supported by ICS: make a new event
var _new = Util.clone(prev);
r.until = getICSDate(d - 1); // Stop previous recursion
delete r.count;
addEvent(ICS, prev, null); // Add previous event
Array.prototype.push.apply(ICS, toAdd); // Add individual updates
toAdd = [];
prev = _new;
if (all) { all = all.slice(idx); }
// if we updated the recurrence rule, count is reset, nothing to do
// if we didn't update the recurrence, we need to fix the count
var _r = _new.recurrenceRule;
if (all && !ud.from.recurrenceRule && _r && _r.count) {
_r.count -= idx;
}
prev.start = d;
prev.end = d + duration;
prev.id = Util.uid();
applyChanges(prev, ud.from);
duration = prev.end - prev.start;
}
if (ud.one) { // Add update
var _one = Util.clone(prev);
_one.start = d;
_one.end = d + duration;
applyChanges(_one, ud.one);
var recId = "RECURRENCE-ID:"+getICSDate(+d);
delete _one.recurrenceRule;
addEvent(toAdd, _one, recId); // Add updated event
}
Array.prototype.push.apply(ICS, [
'BEGIN:VALARM',
'ACTION:DISPLAY',
'DESCRIPTION:This is an event reminder',
'TRIGGER:'+str,
'END:VALARM'
]);
});
}
if (Array.isArray(data.cp_hidden)) {
Array.prototype.push.apply(ICS, data.cp_hidden);
}
ICS.push('END:VEVENT');
addEvent(ICS, prev);
Array.prototype.push.apply(ICS, toAdd); // Add individual updates
});
ICS.push('END:VCALENDAR');
return new Blob([ ICS.join('\n') ], { type: 'text/calendar;charset=utf-8' });
return new Blob([ ICS.join('\r\n') ], { type: 'text/calendar;charset=utf-8' });
};
module.import = function (content, id, cb) {
@ -171,7 +310,7 @@ define([
}
// Store other properties
var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp'];
var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp', 'rrule', 'recurrence-id'];
var hidden = [];
ev.getAllProperties().forEach(function (p) {
if (used.indexOf(p.name) !== -1) { return; }
@ -192,8 +331,25 @@ define([
if (reminders.indexOf(minutes) === -1) { reminders.push(minutes); }
});
// Get recurrence rule
var rrule = ev.getFirstPropertyValue('rrule');
var rec;
if (rrule && rrule.freq) {
rec = {};
rec.freq = rrule.freq.toLowerCase();
if (rrule.interval) { rec.interval = rrule.interval; }
if (rrule.count) { rec.count = rrule.count; }
if (Object.keys(rrule).includes('wkst')) { rec.wkst = (rrule.wkst + 6) % 7; }
if (rrule.until) { rec.until = +new Date(rrule.until); }
Object.keys(rrule.parts || {}).forEach(function (k) {
rec.by = rec.by || {};
var _k = k.toLowerCase().slice(2); // "BYDAY" ==> "day"
rec.by[_k] = rrule.parts[k];
});
}
// Create event
res[uid] = {
var obj = {
calendarId: id,
id: uid,
category: 'time',
@ -203,15 +359,48 @@ define([
start: start,
end: end,
reminders: reminders,
cp_hidden: hidden
cp_hidden: hidden,
};
if (rec) { obj.recurrenceRule = rec; }
if (!hidden.length) { delete res[uid].cp_hidden; }
if (!reminders.length) { delete res[uid].reminders; }
if (!hidden.length) { delete obj.cp_hidden; }
if (!reminders.length) { delete obj.reminders; }
var recId = ev.getFirstPropertyValue('recurrence-id');
if (recId) {
setTimeout(function () {
if (!res[uid]) { return; }
var old = res[uid];
var time = +new Date(recId);
var diff = {};
var from = {};
Object.keys(obj).forEach(function (k) {
if (JSON.stringify(old[k]) === JSON.stringify(obj[k])) { return; }
if (['start','end'].includes(k)) {
diff[k] = Rec.diffDate(old[k], obj[k]);
return;
}
if (k === "recurrenceRule") {
from[k] = obj[k];
return;
}
diff[k] = obj[k];
});
old.recUpdate = old.recUpdate || {one:{},from:{}};
if (Object.keys(from).length) { old.recUpdate.from[time] = from; }
if (Object.keys(diff).length) { old.recUpdate.one[time] = diff; }
});
return;
}
res[uid] = obj;
});
cb(null, res);
// setTimeout to make sure we call back after the "recurrence-id" setTimeout
// are called
setTimeout(function () {
cb(null, res);
});
});
};

File diff suppressed because it is too large Load Diff

View File

@ -6,17 +6,27 @@ define([
'/common/sframe-common-outer.js',
], function (nThen, ApiConfig, DomReady, SFCommonO) {
// Loaded in load #2
var hash, href;
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
SFCommonO.initIframe(waitFor);
var obj = SFCommonO.initIframe(waitFor, true);
href = obj.href;
hash = obj.hash;
}).nThen(function (/*waitFor*/) {
var addData = function (meta) {
meta.calendarHash = Boolean(window.location.hash);
var addData = function (meta, Cryptpad, user, Utils) {
if (hash) {
var parsed = Utils.Hash.parsePadUrl(href);
if (parsed.hashData && parsed.hashData.newPadOpts) {
meta.calendarOpts = Utils.Hash.decodeDataOptions(parsed.hashData.newPadOpts);
}
}
meta.calendarHash = hash;
};
SFCommonO.start({
addData: addData,
hash: hash,
href: href,
noRealtime: true,
cache: true,
});

869
www/calendar/recurrence.js Normal file
View File

@ -0,0 +1,869 @@
define([
'/common/common-util.js',
], function (Util) {
var Rec = {};
var debug = function () {};
// Get week number with any "WKST" (firts day of the week)
// Week 1 is the first week of the year containing at least 4 days in this year
// It depends on which day is considered the first day of the week (default Monday)
// In our case, wkst is a number matching the JS rule: 0 == Sunday
var getWeekNo = Rec.getWeekNo = function (date, wkst) {
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
var newYear = new Date(date.getFullYear(),0,1);
var day = newYear.getDay() - wkst; //the day of week the year begins on
day = (day >= 0 ? day : day + 7);
var daynum = Math.floor((date.getTime() - newYear.getTime())/86400000) + 1;
var weeknum;
// Week 1 / week 53
if (day < 4) {
weeknum = Math.floor((daynum+day-1)/7) + 1;
if (weeknum > 52) {
var nYear = new Date(date.getFullYear() + 1,0,1);
var nday = nYear.getDay() - wkst;
nday = nday >= 0 ? nday : nday + 7;
weeknum = nday < 4 ? 1 : 53;
}
}
else {
weeknum = Math.floor((daynum+day-1)/7);
}
return weeknum;
};
var getYearDay = function (date) {
var start = new Date(date.getFullYear(), 0, 0);
var diff = (date - start) +
((start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000);
var oneDay = 1000 * 60 * 60 * 24;
return Math.floor(diff / oneDay);
};
var setYearDay = function (date, day) {
if (typeof(day) !== "number" || Math.abs(day) < 1 || Math.abs(day) > 366) { return; }
if (day < 0) {
var max = getYearDay(new Date(date.getFullYear(), 11, 31));
day = max + day + 1;
}
date.setMonth(0);
date.setDate(day);
return true;
};
var getEndData = function (s, e) {
if (s > e) { return void console.error("Wrong data"); }
var days;
if (e.getFullYear() === s.getFullYear()) {
days = getYearDay(e) - getYearDay(s);
} else { // eYear < sYear
var tmp = new Date(s.getFullYear(), 11, 31);
var d1 = getYearDay(tmp) - getYearDay(s); // Number of days before December 31st
var de = getYearDay(e);
days = d1 + de;
while ((tmp.getFullYear()+1) < e.getFullYear()) {
tmp.setFullYear(tmp.getFullYear()+1);
days += getYearDay(tmp);
}
}
return {
h: e.getHours(),
m: e.getMinutes(),
days: days
};
};
var setEndData = function (s, e, data) {
e.setTime(+s);
if (!data) { return; }
e.setHours(data.h);
e.setMinutes(data.m);
e.setSeconds(0);
e.setDate(s.getDate() + data.days);
};
var DAYORDER = Rec.DAYORDER = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
var getDayData = function (str) {
var pos = Number(str.slice(0,-2));
var day = DAYORDER.indexOf(str.slice(-2));
return pos ? [pos, day] : day;
};
var goToFirstWeekDay = function (date, wkst) {
var d = date.getDay();
wkst = typeof(wkst) === "number" ? wkst : 1;
if (d >= wkst) {
date.setDate(date.getDate() - (d-wkst));
} else {
date.setDate(date.getDate() - (7+d-wkst));
}
};
var getDateStr = function (date) {
return date.getFullYear() + '-' + (date.getMonth()+1) + '-' + date.getDate();
};
var FREQ = {};
FREQ['daily'] = function (s, i) {
s.setDate(s.getDate()+i);
};
FREQ['weekly'] = function (s,i) {
s.setDate(s.getDate()+(i*7));
};
FREQ['monthly'] = function (s,i) {
s.setMonth(s.getMonth()+i);
};
FREQ['yearly'] = function (s,i) {
s.setFullYear(s.getFullYear()+i);
};
// EXPAND is used to create iterations added from a BYxxx rule
// dateA is the start date and b is the number or id of the BYxxx rule item
var EXPAND = {};
EXPAND['month'] = function (dateS, origin, b) {
var oS = new Date(origin.start);
var a = dateS.getMonth() + 1;
var toAdd = (b-a+12)%12;
var m = dateS.getMonth() + toAdd;
dateS.setMonth(m);
dateS.setDate(oS.getDate());
if (dateS.getMonth() !== m) { return; } // Day 31 may move us to the next month
return true;
};
EXPAND['weekno'] = function (dateS, origin, week, rule) {
var wkst = rule && rule.wkst;
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
var oS = new Date(origin.start);
var lastD = new Date(dateS.getFullYear(), 11, 31); // December 31st
var lastW = getWeekNo(lastD, wkst); // Last week of the year is either 52 or 53
var doubleOne = lastW === 1;
if (lastW === 1) { lastW = 52; }
var a = getWeekNo(dateS, wkst);
if (!week || week > lastW) { return false; } // Week 53 may not exist this year
if (week < 0) { week = lastW + week + 1; } // Turn negative week number into positive
var toAdd = week - a;
var weekS = new Date(+dateS);
// Go to the selected week
weekS.setDate(weekS.getDate() + (toAdd * 7));
goToFirstWeekDay(weekS, wkst);
// Then make sure we are in the correct start day
var all = 'aaaaaaa'.split('').map(function (o, i) {
var date = new Date(+weekS);
date.setDate(date.getDate() + i);
if (date.getFullYear() !== dateS.getFullYear()) { return; }
return date.toLocaleDateString() !== oS.toLocaleDateString() && date;
}).filter(Boolean);
// If we're looking for week 1 and the last week is a week 1, add the days
if (week === 1 && doubleOne) {
goToFirstWeekDay(lastD, wkst);
'aaaaaaa'.split('').some(function (o, i) {
var date = new Date(+lastD);
date.setDate(date.getDate() + i);
if (date.toLocaleDateString() === oS.toLocaleDateString()) { return; }
if (date.getFullYear() > dateS.getFullYear()) { return true; }
all.push(date);
});
}
return all.length ? all : undefined;
};
EXPAND['yearday'] = function (dateS, origin, b) {
var y = dateS.getFullYear();
var state = setYearDay(dateS, b);
if (!state) { return; } // Invalid day "b"
if (dateS.getFullYear() !== y) { return; } // Day 366 make move us to the next year
return true;
};
EXPAND['monthday'] = function (dateS, origin, b, rule) {
if (typeof(b) !== "number" || Math.abs(b) < 1 || Math.abs(b) > 31) { return false; }
var setMonthDay = function (date, day) {
var m = date.getMonth();
if (day < 0) {
var tmp = new Date(date.getFullYear(), date.getMonth()+1, 0); // Last day
day = tmp.getDate() + day + 1;
}
date.setDate(day);
return date.getMonth() === m; // Don't push if day 31 moved us to the next month
};
// Monthly events
if (rule.freq === 'monthly') {
return setMonthDay(dateS, b);
}
var all = 'aaaaaaaaaaaa'.split('').map(function (o, i) {
var date = new Date(dateS.getFullYear(), i, 1);
var ok = setMonthDay(date, b);
return ok ? date : undefined;
}).filter(Boolean);
return all.length ? all : undefined;
};
EXPAND['day'] = function (dateS, origin, b, rule) {
// Here "b" can be a single day ("TU") or a position and a day ("1MO")
var day = getDayData(b);
var pos;
if (Array.isArray(day)) {
pos = day[0];
day = day[1];
}
var all = [];
if (![0,1,2,3,4,5,6].includes(day)) { return false; }
var filterPos = function (m) {
if (!pos) { return; }
var _all = [];
'aaaaaaaaaaaa'.split('').some(function (a, i) {
if (typeof(m) !== "undefined" && i !== m) { return; }
var _pos;
var tmp = all.filter(function (d) {
return d.getMonth() === i;
});
if (pos < 0) {
_pos = tmp.length + pos;
} else {
_pos = pos - 1; // An array starts at 0 but the recurrence rule starts at 1
}
_all.push(tmp[_pos]);
return typeof(m) !== "undefined" && i === m;
});
all = _all.filter(Boolean); // The "5th" {day} won't always exist
};
var tmp;
if (rule.freq === 'yearly') {
tmp = new Date(+dateS);
var y = dateS.getFullYear();
while (tmp.getDay() !== day) { tmp.setDate(tmp.getDate()+1); }
while (tmp.getFullYear() === y) {
all.push(new Date(+tmp));
tmp.setDate(tmp.getDate()+7);
}
filterPos();
return all;
}
if (rule.freq === 'monthly') {
tmp = new Date(+dateS);
var m = dateS.getMonth();
while (tmp.getDay() !== day) { tmp.setDate(tmp.getDate()+1); }
while (tmp.getMonth() === m) {
all.push(new Date(+tmp));
tmp.setDate(tmp.getDate()+7);
}
filterPos(m);
return all;
}
if (rule.freq === 'weekly') {
while (dateS.getDay() !== day) { dateS.setDate(dateS.getDate()+1); }
}
return true;
};
var LIMIT = {};
LIMIT['month'] = function (events, rule) {
return events.filter(function (s) {
return rule.includes(s.getMonth()+1);
});
};
LIMIT['weekno'] = function (events, weeks, rules) {
return events.filter(function (s) {
var wkst = rules && rules.wkst;
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
var lastD = new Date(s.getFullYear(), 11, 31); // December 31st
var lastW = getWeekNo(lastD, wkst); // Last week of the year is either 52 or 53
if (lastW === 1) { lastW = 52; }
var w = getWeekNo(s, wkst);
return weeks.some(function (week) {
if (week > 0) { return week === w; }
return w === (lastW + week + 1);
});
});
};
LIMIT['yearday'] = function (events, days) {
return events.filter(function (s) {
var d = getYearDay(s);
var max = getYearDay(new Date(s.getFullYear(), 11, 31));
return days.some(function (day) {
if (day > 0) { return day === d; }
return d === (max + day + 1);
});
});
};
LIMIT['monthday'] = function (events, rule) {
return events.filter(function (s) {
var r = Util.clone(rule);
// Transform the negative monthdays into positive for this specific month
r = r.map(function (b) {
if (b < 0) {
var tmp = new Date(s.getFullYear(), s.getMonth()+1, 0); // Last day
b = tmp.getDate() + b + 1;
}
return b;
});
return r.includes(s.getDate());
});
};
LIMIT['day'] = function (events, days, rules) {
return events.filter(function (s) {
var dayStr = s.toLocaleDateString();
// Check how to handle position in BYDAY rules (last day of the month or the year?)
var type = 'yearly';
if (rules.freq === 'monthly' ||
(rules.freq === 'yearly' && rules.by && rules.by.month)) {
type = 'monthly';
}
// Check if this event matches one of the allowed days
return days.some(function (r) {
// rule elements are strings with pos and day
var day = getDayData(r);
var pos;
if (Array.isArray(day)) {
pos = day[0];
day = day[1];
}
if (!pos) {
return s.getDay() === day;
}
// If we have a position, we can use EXPAND.day to get the nth {day} of the
// year/month and compare if it matches with
var d = new Date(s.getFullYear(), s.getMonth(), 1);
if (type === 'yearly') { d.setMonth(0); }
var res = EXPAND["day"](d, {}, r, {freq: type});
return res.some(function (date) {
return date.toLocaleDateString() === dayStr;
});
});
});
};
LIMIT['setpos'] = function (events, rule) {
var init = events.slice();
var rules = Util.deduplicateString(rule.slice().map(function (n) {
if (n > 0) { return (n-1); }
if (n === 0) { return; }
return init.length + n;
}));
return events.filter(function (ev) {
var idx = init.indexOf(ev);
return rules.includes(idx);
});
};
var BYORDER = ['month','weekno','yearday','monthday','day'];
var BYDAYORDER = ['month','monthday','day'];
Rec.getMonthId = function (d) {
return d.getFullYear() + '-' + d.getMonth();
};
var cache = window.CP_calendar_cache = {};
var recurringAcross = {};
Rec.resetCache = function () {
cache = window.CP_calendar_cache = {};
recurringAcross = {};
};
var iterate = function (rule, _origin, s) {
// "origin" is the original event to detect the start of BYxxx
var origin = Util.clone(_origin);
var oS = new Date(origin.start);
var id = origin.id.split('|')[0]; // Use same cache when updating recurrence rule
// "uid" is used for the cache
var uid = s.toLocaleDateString();
cache[id] = cache[id] || {};
var inter = rule.interval || 1;
var freq = rule.freq;
var all = [];
var limit = function (byrule, n) {
all = LIMIT[byrule](all, n, rule);
};
var expand = function (byrule) {
return function (n) {
// Set the start date at the beginning of the current FREQ
var _s = new Date(+s);
if (rule.freq === 'yearly') {
// January 1st
_s.setMonth(0);
_s.setDate(1);
} else if (rule.freq === 'monthly') {
_s.setDate(1);
} else if (rule.freq === 'weekly') {
goToFirstWeekDay(_s, rule.wkst);
} else if (rule.freq === 'daily') {
// We don't have < byday rules so we can't expand daily rules
}
var add = EXPAND[byrule](_s, origin, n, rule);
if (!add) { return; }
if (Array.isArray(add)) {
add = add.filter(function (dateS) {
return dateS.toLocaleDateString() !== oS.toLocaleDateString();
});
Array.prototype.push.apply(all, add);
} else {
if (_s.toLocaleDateString() === oS.toLocaleDateString()) { return; }
all.push(_s);
}
};
};
// Manage interval for the next iteration
var it = Util.once(function () {
FREQ[freq](s, inter);
});
var addDefault = function () {
if (freq === "monthly") {
s.setDate(15);
} else if (freq === "yearly" && oS.getMonth() === 1 && oS.getDate() === 29) {
s.setDate(28);
}
it();
var _s = new Date(+s);
if (freq === "monthly" || freq === "yearly") {
_s.setDate(oS.getDate());
if (_s.getDate() !== oS.getDate()) { return; } // If 31st or Feb 29th doesn't exist
if (freq === "yearly" && _s.getMonth() !== oS.getMonth()) { return; }
// FIXME if there is a recUpdate that moves the 31st to the 30th, the event
// will still only be displayed on months with 31 days
}
all.push(_s);
};
if (Array.isArray(cache[id][uid])) {
debug('Get cache', id, uid);
if (freq === "monthly") {
s.setDate(15);
} else if (freq === "yearly" && oS.getMonth() === 1 && oS.getDate() === 29) {
s.setDate(28);
}
it();
return cache[id][uid];
}
if (rule.by && freq === 'yearly') {
var order = BYORDER.slice();
var monthLimit = false;
if (rule.by.weekno || rule.by.yearday || rule.by.monthday || rule.by.day) {
order.shift();
monthLimit = true;
}
var first = true;
order.forEach(function (_order) {
var r = rule.by[_order];
if (!r) { return; }
if (first) {
r.forEach(expand(_order));
first = false;
} else if (_order === "day") {
if (rule.by.yearday || rule.by.monthday || rule.by.weekno) {
limit('day', rule.by.day);
} else {
rule.by.day.forEach(expand('day'));
}
} else {
limit(_order, r);
}
});
if (rule.by.month && monthLimit) {
limit('month', rule.by.month);
}
}
if (rule.by && freq === 'monthly') {
// We're going to compute all the entries for the coming month
if (!rule.by.monthday && !rule.by.day) {
addDefault();
} else if (rule.by.monthday) {
rule.by.monthday.forEach(expand('monthday'));
} else if (rule.by.day) {
rule.by.day.forEach(expand('day'));
}
if (rule.by.month) {
limit('month', rule.by.month);
}
if (rule.by.day && rule.by.monthday) {
limit('day', rule.by.day);
}
}
if (rule.by && freq === 'weekly') {
// We're going to compute all the entries for the coming week
if (!rule.by.day) {
addDefault();
} else {
rule.by.day.forEach(expand('day'));
}
if (rule.by.month) {
limit('month', rule.by.month);
}
}
if (rule.by && freq === 'daily') {
addDefault();
BYDAYORDER.forEach(function (_order) {
var r = rule.by[_order];
if (!r) { return; }
limit(_order, r);
});
}
all.sort(function (a, b) {
return a-b;
});
if (rule.by && rule.by.setpos) {
limit('setpos', rule.by.setpos);
}
if (!rule.by || !Object.keys(rule.by).length) {
addDefault();
} else {
it();
}
var done = [];
all = all.filter(function (newS) {
var start = new Date(+newS).toLocaleDateString();
if (done.includes(start)) { return false; }
done.push(start);
return true;
});
debug('Set cache', id, uid);
cache[id][uid] = all;
return all;
};
var getNextRules = function (obj) {
if (!obj.recUpdate) { return []; }
var _allRules = {};
var _obj = obj.recUpdate.from;
Object.keys(_obj || {}).forEach(function (d) {
var u = _obj[d];
if (u.recurrenceRule) { _allRules[d] = u.recurrenceRule; }
});
return Object.keys(_allRules).sort(function (a, b) { return Number(a)-Number(b); })
.map(function (k) {
var r = Util.clone(_allRules[k]);
if (!FREQ[r.freq]) { return; }
if (r.interval && r.interval < 1) { return; }
r._start = Number(k);
return r;
}).filter(Boolean);
};
Rec.getRecurring = function (months, events) {
if (window.CP_DEV_MODE) { debug = console.warn; }
var toAdd = [];
months.forEach(function (monthId) {
// from 1st day of the month at 00:00 to last day at 23:59:59:999
var ms = monthId.split('-');
var _startMonth = new Date(ms[0], ms[1]);
var _endMonth = new Date(+_startMonth);
_endMonth.setMonth(_endMonth.getMonth() + 1);
_endMonth.setMilliseconds(-1);
debug('Compute month', _startMonth.toLocaleDateString());
var rec = events || [];
rec.forEach(function (obj) {
var _start = new Date(obj.start);
var _end = new Date(obj.end);
var _origin = obj;
var rule = obj.recurrenceRule;
if (!rule) { return; }
var nextRules = getNextRules(obj);
var nextRule = nextRules.shift();
if (_start >= _endMonth) { return; }
// Check the "until" date of the latest rule we can use and stop now
// if the recurrence ends before the current month
var until = rule.until;
var _nextRules = nextRules.slice();
var _nextRule = nextRule;
while (_nextRule && _nextRule._start && _nextRule._start < _startMonth) {
until = nextRule.until;
_nextRule = _nextRules.shift();
}
if (until < _startMonth) { return; }
var endData = getEndData(_start, _end);
if (rule.interval && rule.interval < 1) { return; }
if (!FREQ[rule.freq]) { return; }
/*
// Rule examples
rule.by = {
//month: [1, 4, 5, 8, 12],
//weekno: [1, 2, 4, 5, 32, 34, 35, 50],
//yearday: [1, 2, 29, 30, -2, -1, 250],
//monthday: [1, 2, 3, -3, -2, -1],
//day: ["MO", "WE", "FR"],
//setpos: [1, 2, -1, -2]
};
rule.wkst = 0;
rule.interval = 2;
rule.freq = 'yearly';
rule.count = 10;
*/
debug('Iterate over', obj.title, obj);
debug('Use rule', rule);
var count = rule.count;
var c = 1;
var next = function (start) {
var evS = new Date(+start);
if (count && c >= count) { return; }
debug('Start iteration', evS.toLocaleDateString());
var _toAdd = iterate(rule, obj, evS);
debug('Iteration results', JSON.stringify(_toAdd.map(function (o) { return new Date(o).toLocaleDateString();})));
// Make sure to continue if the current year doesn't provide any result
if (!_toAdd.length) {
if (evS.getFullYear() < _startMonth.getFullYear() ||
evS < _endMonth) {
return void next(evS);
}
return;
}
var stop = false;
var newrule = false;
_toAdd.some(function (_newS) {
// Make event with correct start and end time
var _ev = Util.clone(obj);
_ev.id = _origin.id + '|' + (+_newS);
var _evS = new Date(+_newS);
var _evE = new Date(+_newS);
setEndData(_evS, _evE, endData);
_ev.start = +_evS;
_ev.end = +_evE;
_ev._count = c;
if (_ev.isAllDay && _ev.startDay) { _ev.startDay = getDateStr(_evS); }
if (_ev.isAllDay && _ev.endDay) { _ev.endDay = getDateStr(_evE); }
if (nextRule && _ev.start === nextRule._start) {
newrule = true;
}
var useNewRule = function () {
if (!newrule) { return; }
debug('Use new rule', nextRule);
_ev._count = c;
count = nextRule.count;
c = 1;
evS = +_evS;
obj = _ev;
rule = nextRule;
nextRule = nextRules.shift();
};
if (c >= count) { // Limit reached
debug(_evS.toLocaleDateString(), 'count');
stop = true;
return true;
}
if (_evS >= _endMonth) { // Won't affect us anymore
debug(_evS.toLocaleDateString(), 'endMonth');
stop = true;
return true;
}
if (rule.until && _evS > rule.until) {
debug(_evS.toLocaleDateString(), 'until');
stop = true;
return true;
}
if (_evS < _start) { // "Expand" rules may create events before the _start
debug(_evS.toLocaleDateString(), 'start');
return;
}
c++;
if (_evE < _startMonth) { // Ended before the current month
// Nothing to display but continue the recurrence
debug(_evS.toLocaleDateString(), 'startMonth');
if (newrule) { useNewRule(); }
return;
}
// If a recurring event start and end in different months, make sure
// it is only added once
if ((_evS < _endMonth && _evE >= _endMonth) ||
(_evS < _startMonth && _evE >= _startMonth)) {
if (recurringAcross[_ev.id] && recurringAcross[_ev.id].includes(_ev.start)) {
return;
} else {
recurringAcross[_ev.id] = recurringAcross[_ev.id] || [];
recurringAcross[_ev.id].push(_ev.start);
}
}
// Add this event
toAdd.push(_ev);
if (newrule) {
useNewRule();
return true;
}
});
if (!stop) { next(evS); }
};
next(_start);
debug('Added this month (all events)', toAdd.map(function (ev) {
return new Date(ev.start).toLocaleDateString();
}));
});
});
return toAdd;
};
Rec.getAllOccurrences = function (ev) {
if (!ev.recurrenceRule) { return [ev.start]; }
var r = ev.recurrenceRule;
// In case of infinite recursion, we can't get all
if (!r.until && !r.count) { return false; }
var all = [ev.start];
var d = new Date(ev.start);
d.setDate(15); // Make sure we won't skip a month if the event starts on day > 28
var toAdd = [];
var i = 0;
var check = function () {
return r.count ? (all.length < r.count) : (+d <= r.until);
};
while ((toAdd = Rec.getRecurring([Rec.getMonthId(d)], [ev])) && check() && i < (r.count*12)) {
Array.prototype.push.apply(all, toAdd.map(function (_ev) { return _ev.start; }));
d.setMonth(d.getMonth() + 1);
i++;
}
return all;
};
Rec.diffDate = function (oldTime, newTime) {
var n = new Date(newTime);
var o = new Date(oldTime);
// Diff Days
var d = 0;
var mult = n < o ? -1 : 1;
while (n.toLocaleDateString() !== o.toLocaleDateString() || mult >= 10000) {
n.setDate(n.getDate() - mult);
d++;
}
d = mult * d;
// Diff hours
n = new Date(newTime);
var h = n.getHours() - o.getHours();
// Diff minutes
var m = n.getMinutes() - o.getMinutes();
return {
d: d,
h: h,
m: m
};
};
var sortUpdate = function (obj) {
return Object.keys(obj).sort(function (d1, d2) {
return Number(d1) - Number(d2);
});
};
Rec.applyUpdates = function (events) {
events.forEach(function (ev) {
ev.raw = {
start: ev.start,
end: ev.end,
};
if (!ev.recUpdate) { return; }
var from = ev.recUpdate.from || {};
var one = ev.recUpdate.one || {};
var s = ev.start;
// Add "until" date to our recurrenceRule if it has been modified in future occurences
var nextRules = getNextRules(ev).filter(function (r) {
return r._start > s;
});
var nextRule = nextRules.shift();
var applyDiff = function (obj, k) {
var diff = obj[k]; // Diff is always compared to origin start/end
var d = new Date(ev.raw[k]);
d.setDate(d.getDate() + diff.d);
d.setHours(d.getHours() + diff.h);
d.setMinutes(d.getMinutes() + diff.m);
ev[k] = +d;
};
sortUpdate(from).forEach(function (d) {
if (s < Number(d)) { return; }
Object.keys(from[d]).forEach(function (k) {
if (k === 'start' || k === 'end') { return void applyDiff(from[d], k); }
if (k === "recurrenceRule" && !from[d][k]) { return; }
ev[k] = from[d][k];
});
});
Object.keys(one[s] || {}).forEach(function (k) {
if (k === 'start' || k === 'end') { return void applyDiff(one[s], k); }
if (k === "recurrenceRule" && !one[s][k]) { return; }
ev[k] = one[s][k];
});
if (ev.deleted) {
Object.keys(ev).forEach(function (k) {
delete ev[k];
});
}
if (nextRule && ev.recurrenceRule) {
ev.recurrenceRule._next = nextRule._start - 1;
}
if (ev.reminders) {
ev.raw.reminders = ev.reminders;
}
});
return events;
};
return Rec;
});

View File

@ -21,10 +21,24 @@ html, body {
padding-top: 15px;
}
.summary, .failure, .error, .success {
.cp-test-status, .summary {
margin-bottom: 1em;
border-radius: @corner;
padding: 15px;
&.success {
border: 1px solid green;
}
&.failure {
border: 1px solid red;
}
&.error {
border: 1px solid red;
}
&.warning {
border: 1px solid yellow;
}
}
//.summary, .failure, .error, .success { }
.pending {
border: 1px solid @cryptpad_text_col;
@ -32,15 +46,6 @@ html, body {
margin-right: 20px;
}
}
.success {
border: 1px solid green;
}
.failure {
border: 1px solid red;
}
.error {
border: 1px solid red;
}
.hidden {
display: none;
}
@ -84,14 +89,19 @@ html, body {
word-break: keep-all;
font-style: italic;
}
a {
color: @cryptpad_color_link;
}
}
.cp-notice-browser, .cp-notice-details, .cp-notice-other {
a {
color: @cryptpad_color_link;
}
.cp-notice-browser, .cp-notice-details, .cp-notice-other, .cp-notice-customizations {
font-size: 70%;
}
.cp-notice-customizations {
border: 1px solid #777;
padding: 5px;
}
.underline { text-decoration: underline; }
.cp-app-checkup-version, .cp-app-checkup-browser {
.underline;

View File

@ -893,6 +893,19 @@ define([
});
*/
var parseResponseHeaders = xhr => {
var H = {};
xhr.getAllResponseHeaders()
.split(/\r|\n/)
.filter(Boolean)
.forEach(line => {
line.replace(/([^:]+):(.*)/, (all, key, value) => {
H[key] = value.trim();
});
});
return H;
};
var CSP_DESCRIPTIONS = {
'default-src': '',
'style-src': '',
@ -1469,9 +1482,110 @@ define([
});
});
assert(function (cb, msg) {
// public instances are expected to be open for registration
// if this is not a public instance, pass this test immediately
if (!ApiConfig.listMyInstance) { return cb(true); }
// if it's public but registration is not registricted, that's also a pass
if (!ApiConfig.restrictRegistration) { return void cb(true); }
setWarningClass(msg);
msg.appendChild(h('span', [
"The administrators of this instance have opted in to inclusion in ",
link('https://cryptpad.org/instances/', 'the public instance directory'),
' but have disabled registration, which is expected to be open.',
h('br'),
h('br'),
" Registration can be reopened using the instance's admin panel.",
]));
cb(false);
});
var compareCustomized = function (a, b, cb) {
var getText = (url, done) => {
Tools.common_xhr(url, xhr => {
xhr.done(done);
});
};
var A, B;
nThen(w => {
getText(a, w(res => {
A = res;
}));
getText(b, w(res => {
B = res;
}));
}).nThen(() => {
cb(void 0, A === B);
});
};
var CUSTOMIZATIONS = [];
// check whether some important pages have been customized
assert(function (cb /*, msg */) {
nThen(function (w) {
// add whatever custom pages you want here
[
'application_config.js',
'pages.js',
].forEach(resource => {
// sort this above errors and warnings and style in a neutral color.
var A = `/customize.dist/${resource}`;
var B = `/customize/${resource}`;
compareCustomized(A, B, w((err, same) => {
if (err || same) { return; }
CUSTOMIZATIONS.push(resource);
}));
});
}).nThen(function () {
// Implementing these checks as a test was an easy way to ensure that
// they completed before the final report was shown. It's intentional
// that this always passes
cb(true);
});
});
var serverToken;
Tools.common_xhr('/', function (xhr) {
serverToken = xhr.getResponseHeader('server');
assert(function (cb, msg) {
Tools.common_xhr('/', function (xhr) {
serverToken = xhr.getResponseHeader('server');
msg.appendChild(h('span', [
`Due to its use of `,
h('em', `CloudFlare`),
` this instance may be inaccessible by users of the Tor network, and generally less secure because of the additional point of failure where code can be intercepted and modified by bad actors.`,
]));
//if (1) { return void cb(false || {serverToken}); }
cb(!/cloudflare/i.test(serverToken) || {
serverToken,
});
});
});
assert(function (cb, msg) {
// provide an exception for development instances
if (isLocalhost(trimmedUnsafe) && isLocalhost(window.location.href)) {
return void cb(true);
}
msg.appendChild(h('span', [
'This instance is not configured to require HTTP Strict Transport Security (HSTS) - which instructs clients to only interact with it over a secure connection.',
]));
Tools.common_xhr('/', function (xhr) {
var H = parseResponseHeaders(xhr);
var HSTS = H['strict-transport-security'];
// check for a numerical value of max-age
if (/max\-age=\d+/.test(HSTS)) {
return void cb(true);
}
// else call back with the value
cb(HSTS);
});
});
var row = function (cells) {
@ -1488,11 +1602,11 @@ define([
console.error(err);
}
return h('div.error', [
return h(`div.error.cp-test-status.${obj.type}`, [
h('h5', obj.message),
h('div.table-container',
h('table', [
row(["Failed test number", obj.test + 1]),
row(["Test number", obj.test + 1]),
row(["Returned value", h('pre', code(printableValue))]),
])
),
@ -1536,21 +1650,82 @@ define([
};
Assert.run(function (state) {
var errors = state.errors;
var isWarning = function (x) {
return x && /cp\-warning/.test(x.getAttribute('class'));
};
var isInfo = x => x && /cp\-info/.test(x.getAttribute('class'));
var errors = state.errors; // TODO anomalies might be better?
var categories = {
error: 0,
info: 0,
warning: 0,
};
errors.forEach(obj => {
if (isWarning(obj.message)) {
obj.type = 'warning';
} else if (isInfo(obj.message)) {
obj.type = 'info';
state.passed++;
} else {
obj.type = 'error';
}
Util.inc(categories, obj.type);
});
var failed = errors.length;
Messages.assert_numberOfTestsPassed = "{0} / {1} tests passed.";
var statusClass = failed? 'failure': 'success';
var statusClass;
if (categories.error !== 0) {
statusClass = 'failure';
} else if (categories.warning !== 0) {
statusClass = 'failure';
} else if (categories.info !== 0) {
statusClass = 'neutral';
} else {
statusClass = 'success';
}
var failedDetails = "Details found below";
var successDetails = "This checkup only tests the most common configuration issues. You may still experience errors or incorrect behaviour.";
var details = h('p.cp-notice-details', failed? failedDetails: successDetails);
var sortMethod = function (a, b) {
if (a.type === 'info' && b.type !== 'info') {
return 1;
}
if (a.type === 'warning' && b.type !== 'warning') {
return 1;
}
return a.test - b.test;
};
var customizations;
if (CUSTOMIZATIONS.length) {
customizations = h('div.cp-notice-customizations', [
h('p', `The following assets have been customized for this instance:`),
h('ul', CUSTOMIZATIONS.map(asset => {
var href = `/customize/${asset}`;
return h('li', [
h('a', {
href: `href?${+new Date()}`,
target: '_blank',
}, href),
]);
})),
h('p', `Unexpected behaviour could be related to these changes. If you are this instance's administrator, please try temporarily disabling them before submitting a bug report.`),
]);
}
var summary = h('div.summary.' + statusClass, [
versionStatement(),
serverStatement(serverToken),
browserStatement(),
customizations,
h('p', Messages._getKey('assert_numberOfTestsPassed', [
state.passed,
state.total
@ -1558,17 +1733,6 @@ define([
details,
]);
var isWarning = function (x) {
return x && /cp\-warning/.test(x.getAttribute('class'));
};
var sortMethod = function (a, b) {
if (isWarning(a.message) && !isWarning(b.message)) {
return 1;
}
return a.test - b.test;
};
var report = h('div.report', [
summary,
h('div.failures', errors.sort(sortMethod).map(failureReport)),

View File

@ -68,6 +68,7 @@ define([
'markdown',
'gfm',
'html',
'asciidoc',
'htmlembedded',
'htmlmixed',
'index.html',
@ -143,6 +144,31 @@ define([
previews['htmlmixed'] = function (val, $div, common) {
DiffMd.apply(val, $div, common);
};
previews['asciidoc'] = function (val, $div, common) {
require([
'asciidoctor',
'/lib/highlight/highlight.pack.js',
'css!/lib/highlight/styles/' + (window.CryptPad_theme === 'dark' ? 'dark.css' : 'github.css')
], function (asciidoctor) {
var reg = asciidoctor.Extensions.create();
var Highlight = window.hljs;
reg.inlineMacro('media-tag', function () {
var t = this;
t.process(function (parent, target) {
var d = target.split('|');
return t.createInline(parent, 'quoted', `<media-tag src="${d[0]}" data-crypto-key="${d[1]}"></media-tag>`).convert();
});
});
var html = asciidoctor.convert(val, { attributes: 'showtitle', extension_registry: reg });
DiffMd.apply(html, $div, common);
$div.find('pre code').each(function (i, el) {
Highlight.highlightBlock(el);
});
});
};
var mkPreviewPane = function (editor, CodeMirror, framework, isPresentMode) {
var $previewContainer = $('#cp-app-code-preview');
@ -370,9 +396,17 @@ define([
evModeChange.reg(function (mode) {
if (MEDIA_TAG_MODES.indexOf(mode) !== -1) {
// Embedding is enabled
framework.setMediaTagEmbedder(function (mt) {
framework.setMediaTagEmbedder(function (mt, d) {
editor.focus();
editor.replaceSelection($(mt)[0].outerHTML);
var txt = $(mt)[0].outerHTML;
if (editor.getMode().name === "asciidoc") {
if (d.static) {
txt = d.href + `[${d.name}]`;
} else {
txt = `media-tag:${d.src}|${d.key}[]`;
}
}
editor.replaceSelection(txt);
});
} else {
// Embedding is disabled

View File

@ -352,10 +352,21 @@ define([
});
var linkName, linkPassword, linkMessage, linkError, linkSpinText;
var linkForm, linkSpin, linkResult;
var linkForm, linkSpin, linkResult, linkUses, linkRole;
var linkWarning;
// Invite from link
var dismissButton = h('span.fa.fa-times');
var roleViewer = UI.createRadio('cp-team-role', 'cp-team-role-viewer',
Messages.team_viewers, true, {
input: { value: 'VIEWER' },
});
var roleMember = UI.createRadio('cp-team-role', 'cp-team-role-member',
Messages.team_members, false, {
input: { value: 'MEMBER' },
});
var linkContent = h('div.cp-share-modal', [
h('p', Messages.team_inviteLinkTitle ),
linkError = h('div.alert.alert-danger.cp-teams-invite-alert', {style : 'display: none;'}),
@ -372,7 +383,7 @@ define([
h('div.cp-teams-invite-block', [
h('span', Messages.team_inviteLinkSetPassword),
h('a.cp-teams-help.fa.fa-question-circle', {
href: origin + Pages.localizeDocsLink('https://docs.cryptpad.org/en/user_guide/security.html#passwords-for-documents-and-folders'),
href: Pages.localizeDocsLink('https://docs.cryptpad.org/en/user_guide/security.html#passwords-for-documents-and-folders'),
target: "_blank",
'data-tippy-placement': "right"
})
@ -387,7 +398,21 @@ define([
linkMessage = h('textarea.cp-teams-invite-message', {
placeholder: Messages.team_inviteLinkNoteMsg,
rows: 3
})
}),
linkRole = h('div.cp-teams-invite-block.cp-teams-invite-role',
h('span', Messages.team_inviteRole),
roleViewer,
roleMember
),
h('div.cp-teams-invite-block.cp-teams-invite-uses',
linkUses = h('input', {
type: 'number',
min: 0,
max: 999,
value: 1
}),
h('span', Messages.team_inviteUses)
),
]),
linkSpin = h('div.cp-teams-invite-spinner', {
style: 'display: none;'
@ -407,10 +432,11 @@ define([
dismissButton
])
]);
$(linkUses).on('change keyup', function(e) {
if (e.target.value === '') { e.target.value = 0; }
});
$(linkMessage).keydown(function (e) {
if (e.which === 13) {
e.stopPropagation();
}
if (e.which === 13) { e.stopPropagation(); }
});
var localStore = window.cryptpadStore;
localStore.get('hide-alert-teamInvite', function (val) {
@ -428,6 +454,12 @@ define([
var $nav = $linkContent.closest('.alertify').find('nav');
$(linkError).text('').hide();
var name = $(linkName).val();
var uses = Number($(linkUses).val());
if (isNaN(uses) || !uses) { uses = -1; }
var role = $(linkRole).find("input[name='cp-team-role']:checked").val() || 'VIEWER';
var pw = $(linkPassword).find('input').val();
var msg = $(linkMessage).val();
var hash = Hash.createRandomHash('invite', pw);
@ -461,6 +493,8 @@ define([
hash: hash,
teamId: config.teamId,
seeds: seeds,
role: role,
uses: uses
}, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
@ -1417,7 +1451,7 @@ define([
return /HTML/.test(Object.prototype.toString.call(o)) &&
typeof(o.tagName) === 'string';
};
var allowedTags = ['a', 'p', 'hr', 'div'];
var allowedTags = ['a', 'li', 'p', 'hr', 'div'];
var isValidOption = function (o) {
if (typeof o !== "object") { return false; }
if (isElement(o)) { return true; }
@ -1541,6 +1575,7 @@ define([
$innerblock.find('.cp-dropdown-element-active').removeClass('cp-dropdown-element-active');
if (config.isSelect && value) {
// We use JSON.stringify here to escape quotes
if (typeof(value) === "object") { value = JSON.stringify(value); }
var $val = $innerblock.find('[data-value='+JSON.stringify(value)+']');
setActive($val);
try {

View File

@ -36,6 +36,7 @@
};
Util.clone = function (o) {
if (o === undefined || o === null) { return o; }
return JSON.parse(JSON.stringify(o));
};

View File

@ -125,6 +125,12 @@ define([
formSeed = obj;
}));
}).nThen(function () {
if (!formSeed) { // no drive mode
formSeed = localStorage.CP_formSeed || Hash.createChannelId();
localStorage.CP_formSeed = formSeed;
} else {
delete localStorage.CP_formSeed;
}
cb({
curvePrivate: curvePrivate,
curvePublic: curvePrivate && Hash.getCurvePublicFromPrivate(curvePrivate),
@ -135,28 +141,162 @@ define([
common.getFormAnswer = function (data, cb) {
postMessage("GET", {
key: ['forms', data.channel],
}, cb);
};
common.storeFormAnswer = function (data) {
postMessage("SET", {
key: ['forms', data.channel],
value: {
hash: data.hash,
curvePrivate: data.curvePrivate,
anonymous: data.anonymous
}
}, function (obj) {
if (obj && obj.error) {
if (obj.error === "ENODRIVE") {
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
if (answered.indexOf(data.channel) === -1) { answered.push(data.channel); }
localStorage.CP_formAnswered = JSON.stringify(answered);
return;
}
console.error(obj.error);
if (obj && obj.error === "ENODRIVE") {
var all = Util.tryParse(localStorage.CP_formAnswers || "{}");
return void cb(all[data.channel]);
}
});
if (obj && obj.error) { return void cb(obj); }
if (obj) {
if (!Array.isArray(obj)) { obj = [obj]; }
return void cb(obj);
}
// We have a drive and no answer but maybe we had
// previous "nodrive" answers: migrate
var old = Util.tryParse(localStorage.CP_formAnswers || "{}");
if (Array.isArray(old[data.channel])) {
var d = old[data.channel];
return void postMessage("SET", {
key: ['forms', data.channel],
value: d
}, function (obj) {
// Delete old data if it was correctly stored in the drive
if (obj && obj.error) { return void cb(d); }
delete old[data.channel];
localStorage.CP_formAnswers = JSON.stringify(old);
cb(d);
});
}
cb();
});
};
common.storeFormAnswer = function (data, cb) {
var answer = {
uid: data.uid,
hash: data.hash,
curvePrivate: data.curvePrivate,
anonymous: data.anonymous
};
var answers = [];
Nthen(function (waitFor) {
common.getFormAnswer(data, waitFor(function (obj) {
if (!obj || obj.error) { return; }
answers = obj;
}));
}).nThen(function () {
answers.push(answer);
postMessage("SET", {
key: ['forms', data.channel],
value: answers
}, function (obj) {
if (obj && obj.error) {
if (obj.error === "ENODRIVE") {
var all = Util.tryParse(localStorage.CP_formAnswers || "{}");
all[data.channel] = answers;
localStorage.CP_formAnswers = JSON.stringify(all);
/*
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
if (answered.indexOf(data.channel) === -1) { answered.push(data.channel); }
localStorage.CP_formAnswered = JSON.stringify(answered);
*/
return void cb();
}
console.error(obj.error);
}
cb();
});
});
};
common.deleteFormAnswers = function (data, _cb) {
var cb = Util.once(_cb);
common.getFormAnswer(data, function (obj) {
if (!obj || obj.error) { return void cb(); }
if (!obj.length) { return void cb(); }
var n = Nthen;
var nacl, theirs;
n = n(function (waitFor) {
require(['/bower_components/tweetnacl/nacl-fast.min.js'], waitFor(function () {
nacl = window.nacl;
var s = new Uint8Array(32);
theirs = nacl.box.keyPair.fromSecretKey(s);
}));
}).nThen;
var toDelete = [];
obj.forEach(function (answer) {
if (answer.uid !== data.uid) { return; }
n = n(function (waitFor) {
var hash = answer.hash;
var h = nacl.util.decodeUTF8(hash);
// Make proof
var curve = answer.curvePrivate;
var mySecret = nacl.util.decodeBase64(curve);
var nonce = nacl.randomBytes(24);
var proofBytes = nacl.box(h, nonce, theirs.publicKey, mySecret);
var proof = nacl.util.encodeBase64(nonce) +'|'+ nacl.util.encodeBase64(proofBytes);
var lineData = {
channel: data.channel,
hash: hash,
proof: proof
};
postMessage("DELETE_MAILBOX_MESSAGE", lineData, waitFor(function (obj) {
if (obj && obj.error && obj.error !== 'HASH_NOT_FOUND') {
// If HASH_NOT_FOUND, the message is already deleted
// so we can delete it locally
waitFor.abort();
return void cb(obj);
}
toDelete.push(hash);
}));
}).nThen;
});
n(function () {
obj = obj.filter(function (answer) { return !toDelete.includes(answer.hash); });
if (!obj.length) { obj = undefined; }
postMessage("SET", {
key: ['forms', data.channel],
value: obj
}, function (_obj) {
if (_obj && _obj.error === "ENODRIVE") {
var all = Util.tryParse(localStorage.CP_formAnswers || "{}");
if (obj) { all[data.channel] = obj; }
else { delete all[data.channel]; }
localStorage.CP_formAnswers = JSON.stringify(all);
return void cb();
}
return void cb(_obj);
});
});
});
};
common.muteChannel = function (channel, state, cb) {
var mutedChannels = [];
Nthen(function (waitFor) {
postMessage("GET", {
key: ['mutedChannels'],
}, waitFor(function (obj) {
if (obj && obj.error) { waitFor.abort(); return void cb(obj); }
mutedChannels = obj || [];
}));
}).nThen(function () {
if (state) {
if (!mutedChannels.includes(channel)) {
mutedChannels.push(channel);
}
} else {
mutedChannels = mutedChannels.filter(function (chan) {
return chan !== channel;
});
}
postMessage("SET", {
key: ['mutedChannels'],
value: mutedChannels
}, cb);
});
};
common.makeNetwork = function (cb) {
@ -431,8 +571,8 @@ define([
}, todo);
};
common.clearOwnedChannel = function (channel, cb) {
postMessage("CLEAR_OWNED_CHANNEL", channel, cb);
common.clearOwnedChannel = function (data, cb) {
postMessage("CLEAR_OWNED_CHANNEL", data, cb);
};
// "force" allows you to delete your drive ID
common.removeOwnedChannel = function (data, cb) {
@ -1154,8 +1294,8 @@ define([
pad.onMetadataEvent = Util.mkEvent();
pad.onChannelDeleted = Util.mkEvent();
pad.requestAccess = function (data, cb) {
postMessage("REQUEST_PAD_ACCESS", data, cb);
pad.contactOwner = function (data, cb) {
postMessage("CONTACT_PAD_OWNER", data, cb);
};
pad.giveAccess = function (data, cb) {
postMessage("GIVE_PAD_ACCESS", data, cb);
@ -2385,6 +2525,7 @@ define([
anonHash: LocalStore.getFSHash(),
localToken: tryParsing(localStorage.getItem(Constants.tokenKey)), // TODO move this to LocalStore ?
language: common.getLanguage(),
form_seed: localStorage.CP_formSeed,
cache: rdyCfg.cache,
noDrive: rdyCfg.noDrive,
disableCache: localStorage['CRYPTPAD_STORE|disableCache'],

View File

@ -78,6 +78,7 @@ define([
var TAGS_NAME = Messages.fm_tagsName;
var SHARED_FOLDER = 'sf';
var SHARED_FOLDER_NAME = Messages.fm_sharedFolderName;
var FILTER = "filter";
// Icons
var faFolder = 'cptools-folder';
@ -1149,8 +1150,12 @@ define([
common.getMediaTagPreview(mts, idx);
};
var refresh = APP.refresh = function () {
APP.displayDirectory(currentPath);
var FILTER_BY = "filterBy";
var refresh = APP.refresh = function (cb) {
var type = APP.store[FILTER_BY];
var path = type ? [FILTER, type, currentPath] : currentPath;
APP.displayDirectory(path, undefined, cb);
};
// `app`: true (force open wiht the app), false (force open in preview),
@ -2940,72 +2945,147 @@ define([
$block.find('a.cp-app-drive-new-link, li.cp-app-drive-new-link').click(showLinkModal);
}
$block.find('a.cp-app-drive-new-doc, li.cp-app-drive-new-doc')
.click(function () {
.on('click auxclick', function (e) {
e.preventDefault();
var type = $(this).attr('data-type') || 'pad';
var path = manager.isPathIn(currentPath, [TRASH]) ? '' : currentPath;
openIn(type, path, APP.team);
});
};
var getNewPadOptions = function (isInRoot) {
var options = [];
if (isInRoot) {
options.push({
class: 'cp-app-drive-new-folder',
icon: $folderIcon.clone()[0],
name: Messages.fm_folder,
});
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
options.push({
class: 'cp-app-drive-new-shared-folder',
icon: $sharedFolderIcon.clone()[0],
name: Messages.fm_sharedFolder,
});
}
options.push({ separator: true });
options.push({
class: 'cp-app-drive-new-fileupload',
icon: getIcon('fileupload')[0],
name: Messages.uploadButton,
});
if (APP.allowFolderUpload) {
options.push({
class: 'cp-app-drive-new-folderupload',
icon: getIcon('folderupload')[0],
name: Messages.uploadFolderButton,
});
}
options.push({ separator: true });
options.push({
class: 'cp-app-drive-new-link',
icon: getIcon('link')[0],
name: Messages.fm_link_new,
});
options.push({ separator: true });
}
getNewPadTypes().forEach(function (type) {
var typeClass = 'cp-app-drive-new-doc';
var premium = common.checkRestrictedApp(type);
if (premium < 0) {
typeClass += ' cp-app-hidden cp-app-disabled';
} else if (premium === 0) {
typeClass += ' cp-app-disabled';
}
options.push({
class: typeClass,
type: type,
icon: getIcon(type)[0],
name: Messages.type[type],
});
});
if (APP.store[FILTER_BY]) {
var typeFilter = APP.store[FILTER_BY];
options = options.filter((obj) => {
if (obj.separator) { return false; }
if (typeFilter === 'link') {
return obj.class.includes('cp-app-drive-new-link');
}
if (typeFilter === 'file') {
return obj.class.includes('cp-app-drive-new-fileupload');
}
if (getNewPadTypes().indexOf(typeFilter) !== -1) {
return typeFilter === obj.type;
}
});
}
return options;
};
var createNewButton = function (isInRoot, $container) {
if (!APP.editable) { return; }
if (!APP.loggedIn) { return; } // Anonymous users can use the + menu in the toolbar
if (!manager.isPathIn(currentPath, [ROOT, 'hrefArray'])) { return; }
// Create dropdown
var options = getNewPadOptions(isInRoot).map(function (obj) {
if (obj.separator) { return { tag: 'hr' }; }
var newObj = {
tag: 'a',
attributes: { 'class': obj.class },
content: [ obj.icon, obj.name ]
};
if (obj.type) {
newObj.attributes['data-type'] = obj.type;
newObj.attributes['href'] = APP.origin + Hash.hashToHref('', obj.type);
}
return newObj;
});
var dropdownConfig = {
buttonContent: [
h('i.fa.fa-plus'),
h('span.cp-button-name', Messages.fm_newButton),
],
buttonCls: 'cp-toolbar-dropdown-nowrap',
options: options,
feedback: 'DRIVE_NEWPAD_LOCALFOLDER',
common: common
};
var $block = UIElements.createDropdown(dropdownConfig);
// Custom style:
$block.find('button').addClass('cp-app-drive-toolbar-new');
addNewPadHandlers($block, isInRoot);
$container.append($block);
};
var createFilterButton = function (isTemplate, $container) {
if (!APP.loggedIn) { return; }
// Create dropdown
var options = [];
if (isInRoot) {
if (APP.store[FILTER_BY]) {
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-folder pewpew'},
attributes: {
'class': 'cp-app-drive-rm-filter',
},
content: [
$folderIcon.clone()[0],
Messages.fm_folder,
],
});
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-shared-folder'},
content: [
$sharedFolderIcon.clone()[0],
Messages.fm_sharedFolder,
],
});
}
options.push({tag: 'hr'});
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-fileupload'},
content: [
getIcon('fileupload')[0],
Messages.uploadButton,
],
});
if (APP.allowFolderUpload) {
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-folderupload'},
content: [
getIcon('folderupload')[0],
Messages.uploadFolderButton,
],
});
}
options.push({tag: 'hr'});
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-link'},
content: [
getIcon('link')[0],
Messages.fm_link_new,
h('i.fa.fa-times'),
Messages.fm_rmFilter,
],
});
options.push({tag: 'hr'});
}
getNewPadTypes().forEach(function (type) {
var attributes = {
'class': 'cp-app-drive-new-doc',
'class': 'cp-app-drive-filter-doc',
'data-type': type,
'href': '#'
};
@ -3026,21 +3106,72 @@ define([
],
});
});
if (!isTemplate) {
options.push({tag: 'hr'});
options.push({
tag: 'a',
attributes: {
'class': 'cp-app-drive-filter-doc',
'data-type': 'link'
},
content: [
getIcon('link')[0],
Messages.fm_link_type,
],
});
options.push({
tag: 'a',
attributes: {
'class': 'cp-app-drive-filter-doc',
'data-type': 'file',
'href': '#'
},
content: [
getIcon('file')[0],
Messages.type['file'],
],
});
}
var dropdownConfig = {
buttonContent: [
h('span.fa.fa-plus'),
h('span', Messages.fm_newButton),
h('i.fa.fa-filter'),
h('span.cp-button-name', Messages.fm_filterBy),
],
buttonCls: 'cp-toolbar-dropdown-nowrap',
options: options,
feedback: 'DRIVE_NEWPAD_LOCALFOLDER',
feedback: 'DRIVE_FILTERBY',
common: common
};
if (APP.store[FILTER_BY]) {
var type = APP.store[FILTER_BY];
var message = type === 'link' ? Messages.fm_link_type : Messages.type[type];
dropdownConfig.buttonContent.push(
h('span.cp-button-name', ':'),
getIcon(type)[0],
h('span.cp-button-name', message)
);
}
var $block = UIElements.createDropdown(dropdownConfig);
// Custom style:
$block.find('button').addClass('cp-app-drive-toolbar-new');
// Add style
if (APP.store[FILTER_BY]) {
$block.find('button').addClass('cp-toolbar-button-active');
}
addNewPadHandlers($block, isInRoot);
// Add handlers
if (APP.store[FILTER_BY]) {
$block.find('a.cp-app-drive-rm-filter')
.click(function () {
APP.store[FILTER_BY] = undefined;
APP.displayDirectory(currentPath);
});
}
$block.find('a.cp-app-drive-filter-doc')
.click(function () {
var type = $(this).attr('data-type') || 'invalid-filter';
APP.store[FILTER_BY] = type;
APP.displayDirectory([FILTER, type, currentPath]);
});
$container.append($block);
};
@ -3302,65 +3433,38 @@ define([
return keys;
};
var filterPads = function (files, type, path, useId) {
var root = path && manager.find(path);
return files
.filter(function (e) {
return useId ? manager.isFile(e) : (path && manager.isFile(root[e]));
})
.filter(function (e) {
var id = useId ? e : root[e];
var data = manager.getFileData(id);
if (type === 'link') { return data.static; }
var href = data.href || data.roHref;
return href ? (href.split('/')[1] === type) : true;
// if types are unreachable, display files to avoid misleading the user
});
};
// Create the ghost icon to add pads/folders
var createNewPadIcons = function ($block, isInRoot) {
var $container = $('<div>');
if (isInRoot) {
// Folder
var $element1 = $('<li>', {
'class': 'cp-app-drive-new-folder cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend($folderIcon.clone()).appendTo($container);
$element1.append($('<span>', { 'class': 'cp-app-drive-new-name' })
.text(Messages.fm_folder));
// Shared Folder
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
var $element3 = $('<li>', {
'class': 'cp-app-drive-new-shared-folder cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend($sharedFolderIcon.clone()).appendTo($container);
$element3.append($('<span>', { 'class': 'cp-app-drive-new-name' })
.text(Messages.fm_sharedFolder));
}
// Upload file
var $elementFileUpload = $('<li>', {
'class': 'cp-app-drive-new-fileupload cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(getIcon('fileupload')).appendTo($container);
$elementFileUpload.append($('<span>', {'class': 'cp-app-drive-new-name'})
.text(Messages.uploadButton));
// Upload folder
if (APP.allowFolderUpload) {
var $elementFolderUpload = $('<li>', {
'class': 'cp-app-drive-new-folderupload cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(getIcon('folderupload')).appendTo($container);
$elementFolderUpload.append($('<span>', {'class': 'cp-app-drive-new-name'})
.text(Messages.uploadFolderButton));
}
// Link
var $elementLink = $('<li>', {
'class': 'cp-app-drive-new-link cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(getIcon('link')).appendTo($container);
$elementLink.append($('<span>', {'class': 'cp-app-drive-new-name'})
.text(Messages.fm_link_type));
}
// Pads
getNewPadTypes().forEach(function (type) {
var $element = $('<li>', {
'class': 'cp-app-drive-new-doc cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(getIcon(type)).appendTo($container);
$element.append($('<span>', {'class': 'cp-app-drive-new-name'})
.text(Messages.type[type]));
$element.attr('data-type', type);
getNewPadOptions(isInRoot).forEach(function (obj) {
if (obj.separator) { return; }
var premium = common.checkRestrictedApp(type);
if (premium < 0) {
$element.addClass('cp-app-hidden cp-app-disabled');
} else if (premium === 0) {
$element.addClass('cp-app-disabled');
var $element = $('<li>', {
'class': obj.class + ' cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(obj.icon).appendTo($container);
$element.append($('<span>', { 'class': 'cp-app-drive-new-name' })
.text(obj.name));
if (obj.type) {
$element.attr('data-type', obj.type);
}
});
@ -3435,7 +3539,7 @@ define([
// Unsorted element are represented by "href" in an array: they don't have a filename
// and they don't hav a hierarchical structure (folder/subfolders)
var displayHrefArray = function ($container, rootName, draggable) {
var displayHrefArray = function ($container, rootName, draggable, typeFilter) {
var unsorted = files[rootName];
if (unsorted.length) {
var $fileHeader = getFileListHeader(true);
@ -3445,6 +3549,7 @@ define([
var sortBy = APP.store[SORT_FILE_BY];
sortBy = sortBy === "" ? sortBy = 'name' : sortBy;
var sortedFiles = sortElements(false, [rootName], keys, sortBy, !getSortFileDesc(), true);
sortedFiles = typeFilter ? filterPads(sortedFiles, typeFilter, false, true) : sortedFiles;
sortedFiles.forEach(function (id) {
var file = manager.getFileData(id);
if (!file) {
@ -3526,7 +3631,7 @@ define([
createGhostIcon($container);
};
var displayTrashRoot = function ($list, $folderHeader, $fileHeader) {
var displayTrashRoot = function ($list, $folderHeader, $fileHeader, typeFilter) {
var filesList = [];
var root = files[TRASH];
var isEmpty = true;
@ -3549,14 +3654,25 @@ define([
isEmpty = false;
});
var sortedFolders = typeFilter ? [] : sortTrashElements(true, filesList, null, !getSortFolderDesc());
var sortedFiles = sortTrashElements(false, filesList, APP.store[SORT_FILE_BY], !getSortFileDesc);
if (typeFilter) {
var ids = sortedFiles.map(function (obj) { return obj.element; });
var idsFilter = filterPads(ids, typeFilter, false, true);
sortedFiles = sortedFiles.filter(function (obj) {
return (idsFilter.indexOf(obj.element) !== -1);
});
// prevent trash emptying while filter is active
isEmpty = true;
}
if (!isEmpty) {
var $empty = createEmptyTrashButton();
$content.append($empty);
}
var sortedFolders = sortTrashElements(true, filesList, null, !getSortFolderDesc());
var sortedFiles = sortTrashElements(false, filesList, APP.store[SORT_FILE_BY], !getSortFileDesc());
if (manager.hasSubfolder(root, true)) { $list.append($folderHeader); }
if (!typeFilter && manager.hasSubfolder(root, true)) { $list.append($folderHeader); }
sortedFolders.forEach(function (f) {
var $element = createElement([TRASH], f.spath, root, true);
$list.append($element);
@ -3728,7 +3844,7 @@ define([
});
};
var displayRecent = function ($list) {
var displayRecent = function ($list, typeFilter) {
var filesList = manager.getRecentPads();
var limit = 20;
@ -3744,6 +3860,14 @@ define([
var i = 0;
var channels = [];
if (typeFilter) {
var ids = filesList.map(function (arr) { return arr[0]; });
var idsFilter = filterPads(ids, typeFilter, false, true);
filesList = filesList.filter(function (arr) {
return (idsFilter.indexOf(arr[0]) !== -1);
});
}
$list.append(h('li.cp-app-drive-element-separator', h('span', Messages.drive_active1Day)));
filesList.some(function (arr) {
var id = arr[0];
@ -3932,6 +4056,17 @@ define([
$content.html("");
sel.$selectBox = $('<div>', {'class': 'cp-app-drive-content-select-box'})
.appendTo($content);
var typeFilter;
var isFilter = path[0] === FILTER;
if (isFilter) {
if (path.length < 3) { return; }
typeFilter = path[1];
path = path[2];
currentPath = path;
} else {
APP.store[FILTER_BY] = undefined;
}
var isInRoot = manager.isPathIn(path, [ROOT]);
var inTrash = manager.isPathIn(path, [TRASH]);
var isTrashRoot = manager.comparePath(path, [TRASH]);
@ -3939,6 +4074,8 @@ define([
var isAllFiles = manager.comparePath(path, [FILES_DATA]);
var isVirtual = virtualCategories.indexOf(path[0]) !== -1;
var isSearch = path[0] === SEARCH;
var isRecent = path[0] === RECENT;
var isOwned = path[0] === OWNED;
var isTags = path[0] === TAGS;
// ANON_SHARED_FOLDER
var isSharedFolder = path[0] === SHARED_FOLDER && APP.newSharedFolder;
@ -4040,6 +4177,9 @@ define([
if (!readOnlyFolder) {
createNewButton(isInRoot, APP.toolbar.$bottomL);
}
if (!isTags && !isSearch) {
createFilterButton(isTemplate, APP.toolbar.$bottomL);
}
if (APP.mobile()) {
var $context = $('<button>', {
@ -4075,16 +4215,16 @@ define([
var $fileHeader = getFileListHeader(true);
if (isTemplate) {
displayHrefArray($list, path[0], true);
displayHrefArray($list, path[0], true, typeFilter);
} else if (isAllFiles) {
displayAllFiles($list);
} else if (isTrashRoot) {
displayTrashRoot($list, $folderHeader, $fileHeader);
displayTrashRoot($list, $folderHeader, $fileHeader, typeFilter);
} else if (isSearch) {
displaySearch($list, path[1]);
} else if (path[0] === RECENT) {
displayRecent($list);
} else if (path[0] === OWNED) {
} else if (isRecent) {
displayRecent($list, typeFilter);
} else if (isOwned) {
displayOwned($list);
} else if (isTags) {
displayTags($list);
@ -4093,11 +4233,12 @@ define([
displaySharedFolder($list);
} else {
if (!inTrash) { $dirContent.contextmenu(openContextMenu('content')); }
if (manager.hasSubfolder(root)) { $list.append($folderHeader); }
if (!isFilter && manager.hasSubfolder(root)) { $list.append($folderHeader); }
// display sub directories
var keys = Object.keys(root);
var sortedFolders = sortElements(true, path, keys, null, !getSortFolderDesc());
var sortedFolders = isFilter ? [] : sortElements(true, path, keys, null, !getSortFolderDesc());
var sortedFiles = sortElements(false, path, keys, APP.store[SORT_FILE_BY], !getSortFileDesc());
sortedFiles = isFilter ? filterPads(sortedFiles, typeFilter, path) : sortedFiles;
sortedFolders.forEach(function (key) {
if (manager.isFile(root[key])) { return; }
var $element = createElement(path, key, root, true);
@ -4149,10 +4290,12 @@ define([
appStatus.ready(true);
};
var displayDirectory = APP.displayDirectory = function (path, force) {
var displayDirectory = APP.displayDirectory = function (path, force, cb) {
cb = cb || function () {};
if (APP.closed || (APP.$content && !$.contains(document.documentElement, APP.$content[0]))) { return; }
if (history.isHistoryMode) {
return void _displayDirectory(path, force);
_displayDirectory(path, force);
return void cb();
}
if (!manager.comparePath(currentPath, path)) {
removeSelected();
@ -4161,6 +4304,7 @@ define([
copyObjectValue(files, proxy.drive);
updateSharedFolders(sframeChan, manager, files, folders, function () {
_displayDirectory(path, force);
cb();
});
});
};
@ -5206,8 +5350,9 @@ define([
};
APP.FM = common.createFileManager(fmConfig);
refresh();
UI.removeLoadingScreen();
refresh(function () {
UI.removeLoadingScreen();
});
/*
if (!APP.team) {

View File

@ -102,6 +102,7 @@ define([
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
channels: otherChan,
command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS',
value: [ed],
teamId: teamOwner
@ -335,11 +336,6 @@ define([
}
}).nThen(function (waitFor) {
var href = data.href;
var hashes = priv.hashes || {};
var bestHash = hashes.editHash || hashes.viewHash || hashes.fileHash;
if (data.fakeHref) {
href = Hash.hashToHref(bestHash, priv.app);
}
sel.forEach(function (el) {
var curve = $(el).attr('data-curve');
if (curve === user.curvePublic) { return; }
@ -928,12 +924,7 @@ define([
}
var href = data.href;
var hashes = priv.hashes || {};
var bestHash = hashes.editHash || hashes.viewHash || hashes.fileHash;
if (data.fakeHref) {
href = Hash.hashToHref(bestHash, priv.app);
}
var isNotStored = Boolean(data.fakeHref);
var isNotStored = Boolean(data.isNotStored);
sframeChan.query(q, {
teamId: typeof(owned) !== "boolean" ? owned : undefined,
href: href,
@ -1055,13 +1046,13 @@ define([
var owned = Modal.isOwned(Env, data);
// Request edit access
if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !owned && !opts.calendar && priv.app !== 'form') {
if (common.isLoggedIn() && data.roHref && !owned && !opts.calendar && priv.app !== 'form') {
var requestButton = h('button.btn.btn-secondary.no-margin.cp-access-margin-right',
Messages.requestEdit_button);
var requestBlock = h('p', requestButton);
var $requestBlock = $(requestBlock).hide();
content.push(requestBlock);
sframeChan.query('Q_REQUEST_ACCESS', {
sframeChan.query('Q_CONTACT_OWNER', {
send: false,
metadata: data
}, function (err, obj) {
@ -1072,9 +1063,10 @@ define([
$requestBlock.show().find('button').click(function () {
if (spinner.getState()) { return; }
spinner.spin();
sframeChan.query('Q_REQUEST_ACCESS', {
sframeChan.query('Q_CONTACT_OWNER', {
send: true,
metadata: data
metadata: data,
query: "REQUEST_PAD_ACCESS"
}, function (err, obj) {
if (obj && obj.state) {
UI.log(Messages.requestEdit_sent);

View File

@ -42,11 +42,13 @@ define([
if (err || !val) {
if (opts.access) {
data.password = priv.password;
// Access modal and the pad is not stored: we're not an owner
// so we don't need the correct href, just the type
var h = Hash.createRandomHash(priv.app, priv.password);
data.fakeHref = true;
data.href = base + priv.pathname + '#' + h;
// Access modal and the pad is not stored: get the hashes from outer
var hashes = priv.hashes || {};
data.href = Hash.hashToHref(hashes.editHash || hashes.fileHash, priv.app);
if (hashes.viewHash) {
data.roHref = Hash.hashToHref(hashes.viewHash, priv.app);
}
data.isNotStored = true;
} else {
waitFor.abort();
return void cb(err || 'EEMPTY');

View File

@ -37,20 +37,18 @@ define([
}));
}
if (!data.fakeHref) {
if (data.href) {
$('<label>', {'for': 'cp-app-prop-link'}).text(Messages.editShare).appendTo($d);
$d.append(UI.dialog.selectable(data.href, {
id: 'cp-app-prop-link',
}));
}
if (data.href) {
$('<label>', {'for': 'cp-app-prop-link'}).text(Messages.editShare).appendTo($d);
$d.append(UI.dialog.selectable(data.href, {
id: 'cp-app-prop-link',
}));
}
if (data.roHref && !opts.noReadOnly) {
$('<label>', {'for': 'cp-app-prop-rolink'}).text(Messages.viewShare).appendTo($d);
$d.append(UI.dialog.selectable(data.roHref, {
id: 'cp-app-prop-rolink',
}));
}
if (data.roHref && !opts.noReadOnly) {
$('<label>', {'for': 'cp-app-prop-rolink'}).text(Messages.viewShare).appendTo($d);
$d.append(UI.dialog.selectable(data.roHref, {
id: 'cp-app-prop-rolink',
}));
}
if (data.tags && Array.isArray(data.tags)) {

View File

@ -590,8 +590,9 @@ define([
if (!channel.isFriendChat) { return; }
var curvePublic = channel.curvePublic;
var friend = contactsData[curvePublic] || friendData;
var name = Util.fixHTML(UI.getDisplayName(friend.name || friend.displayName));
var content = h('div', [
UI.setHTML(h('p'), Messages._getKey('contacts_confirmRemove', [Util.fixHTML(friend.name)])),
UI.setHTML(h('p'), Messages._getKey('contacts_confirmRemove', [ name ])),
]);
UI.confirm(content, function (yes) {
if (!yes) { return; }
@ -709,6 +710,7 @@ define([
var curvePublic = info.curvePublic;
contactsData[curvePublic] = info;
if (!Array.isArray(types)) { return; }
if (types.indexOf('displayName') !== -1) {
var name = info.displayName;

View File

@ -12,7 +12,7 @@ define([
"Asterisk asterisk",
"Brainfuck brainfuck .b",
"C text/x-csrc .c",
"C text/x-c++src .cpp",
"C++ text/x-c++src .cpp",
"C-like clike .c",
"Clojure clojure .clj",
"CMake cmake _", /* no extension */
@ -50,7 +50,6 @@ define([
"HTML htmlmixed .html",
"HTTP http _", /* no extension */
"IDL idl .idl",
"JADE jade .jade",
"Java text/x-java .java",
"JavaScript javascript .js",
"Jinja2 jinja2 .j2",

View File

@ -335,6 +335,28 @@ define([
}
};
handlers['FORM_RESPONSE'] = function(common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
var title = Util.fixHTML(msg.content.title || Messages.unknownPad);
var href = msg.content.href;
content.getFormatText = function() {
return Messages._getKey('form_responseNotification', [title]);
};
if (href) {
content.handler = function() {
common.openURL(href);
defaultDismiss(common, data)();
};
}
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
handlers['COMMENT_REPLY'] = function(common, data) {
var content = data.content;
var msg = content.msg;
@ -481,6 +503,18 @@ define([
var missed = content.msg.missed;
var start = msg.start;
var title = Util.fixHTML(msg.title);
content.handler = function () {
var priv = common.getMetadataMgr().getPrivateData();
var time = Util.find(data, ['content', 'msg', 'content', 'start']);
if (priv.app === "calendar" && window.APP && window.APP.moveToDate) {
return void window.APP.moveToDate(time);
}
var url = Hash.hashToHref('', 'calendar');
var optsUrl = Hash.getNewPadURL(url, {
time: time
});
common.openURL(optsUrl);
};
content.getFormatText = function () {
var now = +new Date();

View File

@ -342,10 +342,11 @@ define([
cb(account);
};
// clearOwnedChannel is only used for private chat at the moment
// clearOwnedChannel is only used for private chat and forms
Store.clearOwnedChannel = function (clientId, data, cb) {
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
store.rpc.clearOwnedChannel(data, function (err) {
var s = getStore(data && data.teamId);
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
s.rpc.clearOwnedChannel(data.channel, function (err) {
cb({error:err});
});
};
@ -659,6 +660,7 @@ define([
offline: store.proxy && store.offline,
teams: teams,
plan: store.ready ? (account.plan || '') : undefined,
mutedChannels: proxy.mutedChannels
}
};
cb(JSON.parse(JSON.stringify(metadata)));
@ -1947,42 +1949,23 @@ define([
}).nThen(cb);
};
// requestPadAccess is used to check if we have a way to contact the owner
// of the pad AND to send the request if we want
// data.send === false ==> check if we can contact them
// data.send === true ==> send the request
Store.requestPadAccess = function (clientId, data, cb) {
// contactPadOwner is used to send "REQUEST_ACCESS" messages
// and to notify form owners when sending a response
Store.contactPadOwner = function (clientId, data, cb) {
var owner = data.owner;
// If the owner was not is the pad metadata, check if it is a friend.
// We'll contact the first owner for whom we know the mailbox
/* // TODO decide whether we want to re-enable this feature for our own contacts
// communicate the exception to users that 'muting' won't apply to friends
check mailbox in our contacts is not compatible with the new "mute pad" feature
var owners = data.owners;
if (!owner && Array.isArray(owners)) {
var friends = store.proxy.friends || {};
// If we have friends, check if an owner is one of them (with a mailbox)
if (Object.keys(friends).filter(function (curve) { return curve !== 'me'; }).length) {
owners.some(function (edPublic) {
return Object.keys(friends).some(function (curve) {
if (curve === "me") { return; }
if (edPublic === friends[curve].edPublic &&
friends[curve].notifications) {
owner = friends[curve];
return true;
}
});
});
}
}
*/
// If send is true, send the request to the owner.
if (owner) {
if (data.send) {
store.mailbox.sendTo('REQUEST_PAD_ACCESS', {
channel: data.channel
var sendTo = function (query, msg, user, _cb) {
if (store.mailbox && !data.anon) {
return store.mailbox.sendTo(query, msg, user, _cb);
}
Mailbox.sendToAnon(store.anon_rpc, query, msg, user, _cb);
};
sendTo(data.query, {
channel: data.channel,
data: data.msgData
}, {
channel: owner.notifications,
curvePublic: owner.curvePublic
@ -2179,6 +2162,13 @@ define([
}
};
Store.deleteMailboxMessage = function (clientId, data, cb) {
if (!store.anon_rpc) { return void cb({error: 'RPC_NOT_READY'}); }
store.anon_rpc.send('DELETE_MAILBOX_MESSAGE', data, function (e) {
cb({error:e});
});
};
// GET_FULL_HISTORY from sframe-common-outer
Store.getFullHistory = function (clientId, data, cb) {
var network = store.network;
@ -2450,6 +2440,27 @@ define([
// Clients management
var driveEventClients = [];
// Check if this is a channel that we shouldn't leave when closing the debug app
var alwaysOnline = function (chanId) {
if (!store) { return; }
// Drive
if (store.driveChannel === chanId) { return true; }
// Shared folders
if (SF.isSharedFolderChannel(chanId)) { return true; }
// Teams
if (Util.find(store, ['proxy', 'teams'])) {
var t = Util.find(store, ['proxy', 'teams']) || {};
return Object.keys(t).some(function (id) {
return t[id].channel === chanId;
});
}
// Profile
if (Util.find(store, ['proxy', 'profile', 'href'])) {
return Hash.hrefToHexChannelId(Util.find(store, ['proxy', 'profile', 'href']))
=== chanId;
}
};
var dropChannel = Store.dropChannel = function (chanId) {
console.error('Drop channel', chanId);
@ -2462,6 +2473,14 @@ define([
try {
store.onlyoffice.leavePad(chanId);
} catch (e) { console.error(e); }
try {
if (alwaysOnline(chanId)) {
delete Store.channels[chanId];
return;
}
} catch (e) { console.error(e); }
try {
Cache.leaveChannel(chanId);
} catch (e) { console.error(e); }
@ -2973,6 +2992,9 @@ define([
if (!rt.proxy.uid && store.noDriveUid) {
rt.proxy.uid = store.noDriveUid;
}
if (!rt.proxy.form_seed && data.form_seed) {
rt.proxy.form_seed = data.form_seed;
}
/*
// deprecating localStorage migration as of 4.2.0
var drive = rt.proxy.drive;

View File

@ -4,12 +4,13 @@ define([
'/common/common-constants.js',
'/common/common-realtime.js',
'/common/outer/cache-store.js',
'/calendar/recurrence.js',
'/customize/messages.js',
'/bower_components/nthen/index.js',
'chainpad-listmap',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad/chainpad.dist.js',
], function (Util, Hash, Constants, Realtime, Cache, Messages, nThen, Listmap, Crypto, ChainPad) {
], function (Util, Hash, Constants, Realtime, Cache, Rec, Messages, nThen, Listmap, Crypto, ChainPad) {
var Calendar = {};
var getStore = function (ctx, id) {
@ -90,7 +91,29 @@ define([
});
};
var updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
var getRecurring = function (ev) {
var mid = new Date();
var start = new Date(mid.getFullYear(), mid.getMonth()-1, 15);
var end = new Date(mid.getFullYear(), mid.getMonth()+1, 15);
var startId = Rec.getMonthId(start);
var midId = Rec.getMonthId(mid);
var endId = Rec.getMonthId(end);
var toAdd = Rec.getRecurring([startId, midId, endId], [ev]);
var all = [ev];
Array.prototype.push.apply(all, toAdd);
return Rec.applyUpdates(all);
};
var clearDismissed = function (ctx, uid) {
var h = Util.find(ctx, ['store', 'proxy', 'hideReminders']) || {};
Object.keys(h).filter(function (id) {
return id.indexOf(uid) === 0;
}).forEach(function (id) {
delete h[id];
});
};
var _updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
var now = +new Date();
var ev = Util.clone(_ev);
var uid = ev.id;
@ -101,6 +124,10 @@ define([
}
reminders[uid] = [];
if (_ev.deleted) { return; }
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]) || []; // dismissed
var last = ctx.store.data.lastVisit;
if (ev.isAllDay) {
@ -119,10 +146,11 @@ define([
if (ev.end <= now && !missed) {
// No reminder for past events
delete reminders[uid];
clearDismissed(ctx, uid);
return;
}
var send = function () {
var send = function (d) {
var hide = Util.find(ctx, ['store', 'proxy', 'settings', 'general', 'calendar', 'hideNotif']);
if (hide) { return; }
var ctime = ev.start <= now ? ev.start : +new Date(); // Correct order for past events
@ -133,11 +161,18 @@ define([
missed: Boolean(missed),
content: ev
},
hash: 'REMINDER|'+uid
hash: 'REMINDER|'+uid+'-'+d
}, null, function () {
});
};
var sendNotif = function () { ctx.Store.onReadyEvt.reg(send); };
var sent = false;
var sendNotif = function (delay) {
sent = true;
ctx.Store.onReadyEvt.reg(function () {
send(delay);
});
};
var notifs = ev.reminders || [];
notifs.sort(function (a, b) {
@ -148,6 +183,10 @@ define([
var delay = delayMinutes * 60000;
var time = now + delay;
if (d.some(function (minutes) {
return delayMinutes >= minutes;
})) { return; }
// setTimeout only work with 32bit timeout values. If the event is too far away,
// ignore this event for now
// FIXME: call this function again in xxx days to reload these missing timeout?
@ -156,18 +195,35 @@ define([
// If we're too late to send a notification, send it instantly and ignore
// all notifications that were supposed to be sent even earlier
if (ev.start <= time) {
sendNotif();
sendNotif(delayMinutes);
return true;
}
// It starts in more than "delay": prepare the notification
reminders[uid].push(setTimeout(function () {
sendNotif();
sendNotif(delayMinutes);
}, (ev.start - time)));
});
if (!sent) {
// Remone any existing notification from the UI
ctx.Store.onReadyEvt.reg(function () {
ctx.store.mailbox.hideMessage('reminders', {
hash: 'REMINDER|'+uid
}, null, function () {
});
});
}
};
var updateEventReminders = function (ctx, reminders, ev, useLastVisit) {
var all = getRecurring(Util.clone(ev));
all.forEach(function (_ev) {
_updateEventReminders(ctx, reminders, _ev, useLastVisit);
});
};
var addReminders = function (ctx, id, ev) {
var calendar = ctx.calendars[id];
if (!ev) { return; } // XXX deleted event remote: delete reminders
if (!calendar || !calendar.reminders) { return; }
if (calendar.stores.length === 1 && calendar.stores[0] === 0) { return; }
@ -352,10 +408,20 @@ define([
c.lm = lm;
var proxy = c.proxy = lm.proxy;
var _updateCalled = false;
var _update = function () {
if (_updateCalled) { return; }
_updateCalled = true;
setTimeout(function () {
_updateCalled = false;
update();
});
};
lm.proxy.on('cacheready', function () {
if (!proxy.metadata) { return; }
c.cacheready = true;
setTimeout(update);
_update();
if (cb) { cb(null, lm.proxy); }
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('ready', function (info) {
@ -372,24 +438,39 @@ define([
title: data.title
};
}
setTimeout(update);
_update();
if (cb) { cb(null, lm.proxy); }
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('change', [], function () {
if (!c.ready) { return; }
setTimeout(update);
_update();
}).on('change', ['content'], function (o, n, p) {
if (p.length === 2 && n && !o) { // New event
addReminders(ctx, channel, n);
return void addReminders(ctx, channel, n);
}
if (p.length === 2 && !n && o) { // Deleted event
addReminders(ctx, channel, {
return void addReminders(ctx, channel, {
id: p[1],
start: 0
});
}
if (p.length === 3 && n && o && p[2] === 'start') { // Update event start
setTimeout(function () {
if (p.length >= 3 && ['start','reminders','isAllDay'].includes(p[2])) {
// Updated event
return void setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
if (p.length >= 6 && ['start','reminders','isAllDay'].includes(p[5])) {
// Updated recurring event
return void setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
}).on('remove', ['content'], function (x, p) {
_update();
if ((p.length >= 3 && p[2] === 'reminders') ||
(p.length >= 6 && p[5] === 'reminders')) {
return void setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
@ -400,10 +481,10 @@ define([
updateLocalCalendars(ctx, c, md);
}).on('disconnect', function () {
c.offline = true;
setTimeout(update);
_update();
}).on('reconnect', function () {
c.offline = false;
setTimeout(update);
_update();
}).on('error', function (info) {
if (!info || !info.error) { return; }
if (info.error === "EDELETED" ) {
@ -411,7 +492,7 @@ define([
}
if (info.error === "ERESTRICTED" ) {
c.restricted = true;
setTimeout(update);
_update();
}
cb(info);
});
@ -760,8 +841,11 @@ define([
var ev = c.proxy.content[data.ev.id];
if (!ev) { return void cb({error: "EINVAL"}); }
data.rawData = data.rawData || {};
// update the event
var changes = data.changes || {};
var type = data.type || {};
var newC;
if (changes.calendarId) {
@ -770,7 +854,122 @@ define([
newC.proxy.content = newC.proxy.content || {};
}
var RECUPDATE = {
one: {},
from: {}
};
if (['one','from','all'].includes(type.which)) {
ev.recUpdate = ev.recUpdate || RECUPDATE;
if (!ev.recUpdate.one) { ev.recUpdate.one = {}; }
if (!ev.recUpdate.from) { ev.recUpdate.from = {}; }
}
var update = ev.recUpdate;
var alwaysAll = ['calendarId'];
var keys = Object.keys(changes).filter(function (s) {
// we can only change the calendar or recurrence rule on the origin
return !alwaysAll.includes(s);
});
// Delete (future) affected keys
var cleanAfter = function (time) {
[update.from, update.one].forEach(function (obj) {
Object.keys(obj).forEach(function (d) {
if (Number(d) < time) { return; }
delete obj[d];
});
});
};
var cleanKeys = function (obj, when) {
Object.keys(obj).forEach(function (d) {
if (when && Number(d) < when) { return; }
keys.forEach(function (k) {
delete obj[d][k];
});
});
};
// Update recurrence rule. We may create a new event here
var dontSendUpdate = false;
if (typeof(changes.recurrenceRule) !== "undefined") {
if (['one','from'].includes(type.which) && !data.rawData.isOrigin) {
cleanAfter(type.when);
} else {
update = ev.recUpdate = RECUPDATE;
}
}
if (type.which === "one") {
update.one[type.when] = update.one[type.when] || {};
// Nothing to delete
} else if (type.which === "from") {
update.from[type.when] = update.from[type.when] || {};
// Delete all "single/from" updates (affected keys only) after this "from" date
cleanKeys(update.from, type.when);
cleanKeys(update.one, type.when);
} else if (type.which === "all") {
// Delete all "single/from" updates (affected keys only) after
cleanKeys(update.from);
cleanKeys(update.one);
}
if (changes.start && update && (!type.which || type.which === "all")) {
var diff = changes.start - ev.start;
var newOne = {};
var newFrom = {};
Object.keys(update.one || {}).forEach(function (time) {
newOne[Number(time)+diff] = update.one[time];
});
Object.keys(update.from || {}).forEach(function (time) {
newFrom[Number(time)+diff] = update.from[time];
});
update.one = newOne;
update.from = newFrom;
}
// Clear the "dismissed" reminders when the user is updating reminders
var h = Util.find(ctx, ['store', 'proxy', 'hideReminders']) || {};
if (changes.reminders) {
if (type.which === 'one') {
if (!type.when || type.when === ev.start) { delete h[data.ev.id]; }
else { delete h[data.ev.id +'|'+ type.when]; }
} else if (type.which === "from") {
Object.keys(h).filter(function (id) {
return id.indexOf(data.ev.id) === 0;
}).forEach(function (id) {
var time = Number(id.split('|')[1]);
if (!time) { return; }
if (time < type.when) { return; }
delete h[id];
});
} else {
Object.keys(h).filter(function (id) {
return id.indexOf(data.ev.id) === 0;
}).forEach(function (id) {
delete h[id];
});
}
}
// Apply the changes
Object.keys(changes).forEach(function (key) {
if (!alwaysAll.includes(key) && type.which === "one") {
if (key === "recurrenceRule") {
if (data.rawData && data.rawData.isOrigin) {
return (ev[key] = changes[key]);
}
// Always "from", never "one" for recurrence rules
update.from[type.when] = update.from[type.when] || {};
return (update.from[type.when][key] = changes[key]);
}
update.one[type.when][key] = changes[key];
return;
}
if (!alwaysAll.includes(key) && type.which === "from") {
update.from[type.when][key] = changes[key];
return;
}
ev[key] = changes[key];
});
@ -790,6 +989,7 @@ define([
delete c.proxy.content[data.ev.id];
}
nThen(function (waitFor) {
Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor());
if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); }
@ -806,8 +1006,8 @@ define([
addReminders(ctx, id, ev);
}
sendUpdate(ctx, c);
if (newC) { sendUpdate(ctx, newC); }
if (!dontSendUpdate || newC) { sendUpdate(ctx, c); }
if (newC && !dontSendUpdate) { sendUpdate(ctx, newC); }
cb();
});
};
@ -816,7 +1016,22 @@ define([
var c = ctx.calendars[id];
if (!c) { return void cb({error: "ENOENT"}); }
c.proxy.content = c.proxy.content || {};
delete c.proxy.content[data.id];
var evId = data.id.split('|')[0];
if (data.id === evId) {
delete c.proxy.content[data.id];
} else {
var ev = c.proxy.content[evId];
var s = data.raw && data.raw.start;
if (s) {
ev.recUpdate = ev.recUpdate || {
one: {},
from: {}
};
ev.recUpdate.one[s] = {
deleted: true
};
}
}
Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
addReminders(ctx, id, {
id: data.id,
@ -867,6 +1082,20 @@ define([
openChannels(ctx);
}));
ctx.store.proxy.on('change', ['hideReminders'], function (o,n,p) {
var uid = p[1].split('|')[0];
Object.keys(ctx.calendars).some(function (calId) {
var c = ctx.calendars[calId];
if (!c || !c.proxy || !c.proxy.content) { return; }
if (c.proxy.content[uid]) {
setTimeout(function () {
addReminders(ctx, calId, c.proxy.content[uid]);
});
return true;
}
});
});
calendar.closeTeam = function (teamId) {
Object.keys(ctx.calendars).forEach(function (id) {
var ctxCal = ctx.calendars[id];

View File

@ -1,5 +1,5 @@
(function () {
var factory = function (Util, Cred, Nacl) {
var factory = function (Util, Cred, Nacl, Crypto) {
var Invite = {};
var encode64 = Nacl.util.encodeBase64;
@ -49,6 +49,24 @@ var factory = function (Util, Cred, Nacl) {
roster.invite(toInvite, cb);
};
// Invite links should only be visible to members or above, so
// we store them in the roster encrypted with a string only available
// to users with edit rights
var decodeUTF8 = Nacl.util.decodeUTF8;
Invite.encryptHash = function (data, seedStr) {
var array = decodeUTF8(seedStr);
var bytes = Nacl.hash(array);
var cryptKey = bytes.subarray(0, 32);
return Crypto.encrypt(data, cryptKey);
};
Invite.decryptHash = function (encryptedStr, seedStr) {
var array = decodeUTF8(seedStr);
var bytes = Nacl.hash(array);
var cryptKey = bytes.subarray(0, 32);
return Crypto.decrypt(encryptedStr, cryptKey);
};
/* INPUTS
* password (for scrypt)
@ -84,16 +102,17 @@ var factory = function (Util, Cred, Nacl) {
module.exports = factory(
require("../common-util"),
require("../common-credential.js"),
require("nthen"),
require("tweetnacl/nacl-fast")
require("tweetnacl/nacl-fast"),
require("chainpad-crypto/crypto")
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/common/common-credential.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Cred) {
return factory(Util, Cred, window.nacl);
], function (Util, Cred, Crypto) {
return factory(Util, Cred, window.nacl, Crypto);
});
}
}());

View File

@ -20,6 +20,10 @@ define([
if (!curvePublic) { return false; }
return Boolean(muted[curvePublic]);
};
var isChannelMuted = function (ctx, channel) {
var muted = ctx.store.proxy.mutedChannels || [];
return muted.includes(channel);
};
// Store the friend request displayed to avoid duplicates
var friendRequest = {};
@ -575,6 +579,61 @@ define([
cb();
};
// Hide duplicates when receiving a form notification:
// Keep only one notification per channel
var formNotifs = {};
handlers['FORM_RESPONSE'] = function (ctx, box, data, cb) {
var msg = data.msg;
var hash = data.hash;
var content = msg.content;
var channel = content.channel;
if (!channel) { return void cb(true); }
if (isChannelMuted(ctx, channel)) { return void cb(true); }
var title, href;
ctx.Store.getAllStores().some(function (s) {
var res = s.manager.findChannel(channel);
// Check if the pad is in our drive
return res.some(function (obj) {
if (!obj.data) { return; }
if (href && !obj.data.href) { return; } // We already have the VIEW url, we need EDIT
href = obj.data.href || obj.data.roHref;
title = obj.data.filename || obj.data.title;
if (obj.data.href) { return true; } // Abort only if we have the EDIT url
});
});
// If we don't have the edit url, ignore this notification
if (!href) { return void cb(true); }
// Add the title
content.href = href;
content.title = title;
// Remove duplicates
var old = formNotifs[channel];
var toRemove = old ? old.data : undefined;
// Update the data
formNotifs[channel] = {
data: {
type: box.type,
hash: hash
}
};
cb(false, toRemove);
};
removeHandlers['FORM_RESPONSE'] = function (ctx, box, data, hash) {
var content = data.content;
var channel = content.channel;
var old = formNotifs[channel];
if (old && old.data && old.data.hash === hash) {
delete formNotifs[channel];
}
};
// Hide duplicates when receiving a SHARE_PAD notification:
// Keep only one notification per channel: the stronger and more recent one
var comments = {};

View File

@ -164,6 +164,22 @@ proxy.mailboxes = {
});
});
};
Mailbox.sendToAnon = function (anonRpc, type, msg, user, cb) {
var Nacl = Crypto.Nacl;
var curveSeed = Nacl.randomBytes(32);
var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
var curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
var curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);
sendTo({
store: {
anon_rpc: anonRpc,
proxy: {
curvePrivate: curvePrivate,
curvePublic: curvePublic
}
}
}, type, msg, user, cb);
};
// Mark a message as read
var dismiss = function (ctx, data, cId, cb) {
@ -177,6 +193,15 @@ proxy.mailboxes = {
hideMessage(ctx, type, hash, ctx.clients.filter(function (clientId) {
return clientId !== cId;
}));
var uid = hash.slice(9).split('-')[0];
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]);
if (!d) {
var h = ctx.store.proxy.hideReminders = ctx.store.proxy.hideReminders || {};
d = h[uid] = h[uid] || [];
}
var delay = hash.split('-')[1];
if (delay && !d.includes(delay)) { d.push(Number(delay)); }
return;
}
@ -590,6 +615,9 @@ proxy.mailboxes = {
});
};
mailbox.hideMessage = function (type, msg) {
hideMessage(ctx, type, msg.hash, ctx.clients);
};
mailbox.showMessage = function (type, msg, cId, cb) {
if (type === "reminders" && msg) {
ctx.boxes.reminders.content[msg.hash] = msg.msg;

View File

@ -238,6 +238,8 @@ define([
var removeFromFriendList = function (ctx, curvePublic, cb) {
var proxy = ctx.store.proxy;
var friends = proxy.friends;
// FIXME this probably shouldn't happen, but functions that take callbacks
// should be guaranteed to call back.
if (!friends) { return; }
delete friends[curvePublic];
Realtime.whenRealtimeSyncs(ctx.store.realtime, function () {
@ -447,7 +449,12 @@ define([
var msg = [Types.unfriend, proxy.curvePublic, +new Date()];
var msgStr = JSON.stringify(msg);
var cryptMsg = channel.encrypt(msgStr);
channel.wc.bcast(cryptMsg).then(function () {}, function (err) {
channel.wc.bcast(cryptMsg).then(function () {
onFriendRemoved(ctx, curvePublic, data.channel);
removeFromFriendList(ctx, curvePublic, function () {
cb();
});
}, function (err) {
if (err) { return void cb({error:err}); }
onFriendRemoved(ctx, curvePublic, data.channel);
removeFromFriendList(ctx, curvePublic, function () {

View File

@ -463,9 +463,22 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto, Feedback)
if (typeof(members[curve]) !== 'undefined') { throw new Error("MEMBER_ALREADY_PRESENT"); }
// copy the new profile from the old one
members[curve] = Util.clone(members[author]);
// and erase the old one
delete members[author];
var clone = Util.clone(members[author]);
delete clone.remaining;
delete clone.totalUses;
delete clone.inviteChannel;
delete clone.previewChannel;
members[curve] = clone;
// XXX
var remaining = members[author].remaining || 1;
if (remaining === -1) { return true; } // Infinite uses, keep the link
if (remaining > 1) { // Remove 1 use
members[author].remaining = remaining - 1;
} else { // Disable link
delete members[author];
}
return true;
};

View File

@ -385,5 +385,9 @@ define([
});
};
SF.isSharedFolderChannel = function (chanId) {
return Object.keys(allSharedFolders).includes(chanId);
};
return SF;
});

View File

@ -79,7 +79,7 @@ define([
GET_HISTORY: Store.getHistory,
GET_HISTORY_RANGE: Store.getHistoryRange,
IS_NEW_CHANNEL: Store.isNewChannel,
REQUEST_PAD_ACCESS: Store.requestPadAccess,
CONTACT_PAD_OWNER: Store.contactPadOwner,
GIVE_PAD_ACCESS: Store.givePadAccess,
BURN_PAD: Store.burnPad,
GET_PAD_METADATA: Store.getPadMetadata,
@ -88,6 +88,7 @@ define([
GET_LAST_HASH: Store.getLastHash,
GET_SNAPSHOT: Store.getSnapshot,
CORRUPTED_CACHE: Store.corruptedCache,
DELETE_MAILBOX_MESSAGE: Store.deleteMailboxMessage,
// Drive
DRIVE_USEROBJECT: Store.userObjectCommand,
// Settings,

View File

@ -1038,6 +1038,17 @@ define([
});
}
// Decrypt hash for invite links
Object.keys(members).forEach(function (curve) {
var member = members[curve];
if (!member.inviteChannel) { return; }
if (!member.hash) { return; }
if (!teamData.hash) { delete member.hash; return; }
try {
member.hash = Invite.decryptHash(member.hash, teamData.hash);
} catch (e) { console.error(e); }
});
cb(members);
});
};
@ -1580,10 +1591,12 @@ define([
var message = data.message;
var name = data.name;
/*
var password = data.password;
//var password = data.password;
var hash = data.hash;
*/
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
try {
var encryptedHash = Invite.encryptHash(hash, teamData.hash);
} catch (e) { console.error(e); }
// derive { channel, cryptKey} for the preview content channel
var previewKeys = Invite.derivePreviewKeys(seeds.preview);
@ -1595,6 +1608,10 @@ define([
// and a placeholder in the roster
var ephemeralKeys = Invite.generateKeys();
// Initial role of the invited users
var role = data.role || "VIEWER";
var uses = data.uses || 1;
nThen(function (w) {
@ -1652,9 +1669,12 @@ define([
};
putOpts.metadata.validateKey = sign.validateKey;
// available only with the link and the content
var inviteContent = {
teamData: getInviteData(ctx, teamId, false),
teamData: getInviteData(ctx, teamId, role === "MEMBER"),
ephemeral: {
edPublic: ephemeralKeys.edPublic,
edPrivate: ephemeralKeys.edPrivate,
@ -1692,6 +1712,10 @@ define([
curvePublic: ephemeralKeys.curvePublic,
displayName: data.name,
pending: true,
remaining: uses,
totalUses: uses,
role: role,
hash: encryptedHash,
inviteChannel: inviteKeys.channel,
previewChannel: previewKeys.channel,
}
@ -1821,20 +1845,22 @@ define([
}));
}).nThen(function () {
var tempRpc = {};
initRpc(ctx, tempRpc, inviteContent.ephemeral, function (err) {
if (err) { return; }
var rpc = tempRpc.rpc;
if (rosterState.inviteChannel) {
rpc.removeOwnedChannel(rosterState.inviteChannel, function (err) {
if (err) { console.error(err); }
});
}
if (rosterState.previewChannel) {
rpc.removeOwnedChannel(rosterState.previewChannel, function (err) {
if (err) { console.error(err); }
});
}
});
if (!rosterState.remaining || rosterState.remaining === 1) {
initRpc(ctx, tempRpc, inviteContent.ephemeral, function (err) {
if (err) { return; }
var rpc = tempRpc.rpc;
if (rosterState.inviteChannel) {
rpc.removeOwnedChannel(rosterState.inviteChannel, function (err) {
if (err) { console.error(err); }
});
}
if (rosterState.previewChannel) {
rpc.removeOwnedChannel(rosterState.previewChannel, function (err) {
if (err) { console.error(err); }
});
}
});
}
// Add the team to our list and join...
joinTeam(ctx, {
team: inviteContent.teamData

View File

@ -304,6 +304,10 @@ define([
var newParent = exp.find(path);
var tempName = exp.isFile(element) ? Hash.createChannelId() : key;
var newName = exp.getAvailableName(newParent, tempName);
if (Array.isArray(newParent)) {
newParent.push(element);
return;
}
newParent[newName] = element;
};

View File

@ -171,10 +171,12 @@ define([
var data;
// apparently some browser extensions send messages to random targets
// which can trigger parse errors that interrupt normal behaviour
// we therefore log a warning and ignore any messages we can't parse
try {
data = typeof(msg.data) === "object" ? msg.data : JSON.parse(msg.data);
} catch (err) {
console.error(err);
console.warn(err);
return;
}
if (typeof(data.ack) !== "undefined") {
if (acks[data.txid]) { acks[data.txid](!data.ack); }

View File

@ -249,7 +249,6 @@ var factory = function (Util, Rpc) {
}, cb);
};
cb(e, exp);
});
};

View File

@ -20,7 +20,9 @@ define([
'netflux-client': '/bower_components/netflux-websocket/netflux-client',
'chainpad-netflux': '/bower_components/chainpad-netflux/chainpad-netflux',
'chainpad-listmap': '/bower_components/chainpad-listmap/chainpad-listmap',
'cm-extra': '/lib/codemirror-extra-modes'
'cm-extra': '/lib/codemirror-extra-modes',
// asciidoctor same
'asciidoctor': '/lib/asciidoctor/asciidoctor.min'
},
map: {
'*': {

View File

@ -80,7 +80,7 @@ define([
try {
var val = JSON.parse(states[idx].getContent().doc);
var md = config.extractMetadata(val);
var users = Object.keys(md.users).sort();
var users = Object.keys(md.users || {}).sort();
return users.join();
} catch (e) {
console.error(e);

View File

@ -13,6 +13,7 @@ define([
Mailbox.create = function (Common) {
var mailbox = Common.mailbox;
var sframeChan = Common.getSframeChannel();
var priv = Common.getMetadataMgr().getPrivateData();
var execCommand = function (cmd, data, cb) {
sframeChan.query('Q_MAILBOX_COMMAND', {
@ -67,6 +68,14 @@ define([
}
} else if (data.type === 'reminders') {
avatar = h('i.fa.fa-calendar.cp-broadcast.preview');
if (priv.app !== 'calendar') { avatar.classList.add('cp-reminder'); }
$(avatar).click(function (e) {
e.stopPropagation();
if (data.content && data.content.handler) {
return void data.content.handler();
}
Common.openURL(Hash.hashToHref('', 'calendar'));
});
} else if (userData && typeof(userData) === "object" && userData.profile) {
avatar = h('span.cp-avatar');
Common.displayAvatar($(avatar), userData.avatar, userData.displayName || userData.name);
@ -120,7 +129,8 @@ define([
onViewedHandlers.push(function (data) {
var hash = data.hash.replace(/"/g, '\\\"');
var $notif = $('.cp-notification[data-hash="'+hash+'"]:not(.cp-app-notification-archived)');
if (/^REMINDER\|/.test(hash)) { hash = hash.split('-')[0]; }
var $notif = $('.cp-notification[data-hash^="'+hash+'"]:not(.cp-app-notification-archived)');
if ($notif.length) {
$notif.remove();
}

View File

@ -710,6 +710,12 @@ define([
additionalPriv.registeredOnly = true;
}
if (metaObj.priv && Array.isArray(metaObj.priv.mutedChannels)
&& metaObj.priv.mutedChannels.includes(secret.channel)) {
delete metaObj.priv.mutedChannes;
additionalPriv.isChannelMuted = true;
}
var priv = metaObj.priv;
var _plan = typeof(priv.plan) === "undefined" ? Utils.LocalStore.getPremium() : priv.plan;
var p = Utils.Util.checkRestrictedApp(parsed.type, AppConfig,
@ -942,7 +948,7 @@ define([
var metadata = data.metadata;
var add = data.add;
var _secret = secret;
if (metadata && (metadata.href || metadata.roHref) && !metadata.fakeHref) {
if (metadata && (metadata.href || metadata.roHref)) {
var _parsed = Utils.Hash.parsePadUrl(metadata.href || metadata.roHref);
_secret = Utils.Hash.getSecrets(_parsed.type, _parsed.hash, metadata.password);
}
@ -1010,9 +1016,9 @@ define([
});
});
// REQUEST_ACCESS is used both to check IF we can contact an owner (send === false)
// CONTACT_OWNER is used both to check IF we can contact an owner (send === false)
// AND also to send the request if we want (send === true)
sframeChan.on('Q_REQUEST_ACCESS', function (data, cb) {
sframeChan.on('Q_CONTACT_OWNER', function (data, cb) {
if (readOnly && hashes.editHash) {
return void cb({error: 'ALREADYKNOWN'});
}
@ -1030,8 +1036,6 @@ define([
var crypto = Crypto.createEncryptor(_secret.keys);
nThen(function (waitFor) {
// Try to get the owner's mailbox from the pad metadata first.
// If it's is an older owned pad, check if the owner is a friend
// or an acquaintance (from async-store directly in requestAccess)
var todo = function (obj) {
owners = obj.owners;
@ -1065,11 +1069,13 @@ define([
}));
}).nThen(function () {
// If we are just checking (send === false) and there is a mailbox field, cb state true
// If there is no mailbox, we'll have to check if an owner is a friend in the worker
if (!send) { return void cb({state: Boolean(owner)}); }
Cryptpad.padRpc.requestAccess({
Cryptpad.padRpc.contactOwner({
send: send,
anon: data.anon,
query: data.query,
msgData: data.msgData,
channel: _secret.channel,
owner: owner,
owners: owners

View File

@ -663,49 +663,6 @@ MessengerUI, Messages, Pages) {
return $shareBlock;
};
/*
var createRequest = function (toolbar, config) {
if (!config.metadataMgr) {
throw new Error("You must provide a `metadataMgr` to display the request access button");
}
// We can only requets more access if we're in read-only mode
if (config.readOnly !== 1) { return; }
var $requestBlock = $('<button>', {
'class': 'fa fa-lock cp-toolbar-share-button',
title: Messages.requestEdit_button
}).hide();
// If we have access to the owner's mailbox, display the button and enable it
// false => check if we can contact the owner
// true ==> send the request
Common.getSframeChannel().query('Q_REQUEST_ACCESS', {send:false}, function (err, obj) {
if (obj && obj.state) {
var locked = false;
$requestBlock.show().click(function () {
if (locked) { return; }
locked = true;
Common.getSframeChannel().query('Q_REQUEST_ACCESS', {send:true}, function (err, obj) {
if (obj && obj.state) {
UI.log(Messages.requestEdit_sent);
$requestBlock.hide();
} else {
locked = false;
}
});
});
}
});
toolbar.$leftside.append($requestBlock);
toolbar.request = $requestBlock;
return $requestBlock;
};
*/
var createTitle = function (toolbar, config) {
var $titleContainer = $('<span>', {
'class': TITLE_CLS

View File

@ -1,5 +1,5 @@
{
"main_title": "CryptPad: Zusammenarbeit in Echtzeit ohne Preisgabe von Informationen",
"main_title": "CryptPad: Zusammenarbeit in Echtzeit mit Schutz der Privatsphäre",
"type": {
"pad": "Rich Text",
"code": "Code",
@ -831,7 +831,7 @@
"team_inviteLinkNote": "Persönliche Nachricht hinzufügen",
"team_inviteLinkNoteMsg": "Diese Nachricht wird angezeigt, bevor der Empfänger entscheidet, ob er diesem Team beitreten möchte.",
"team_inviteLinkLoading": "Dein Link wird generiert",
"team_inviteLinkWarning": "Die erste Person, die auf diesen Link zugreift, kann diesem Team beitreten und dessen Inhalte einsehen. Teile ihn sorgfältig.",
"team_inviteLinkWarning": "Personen, die auf diesen Link zugreifen, können diesem Team beitreten und dessen Inhalte einsehen. Teile ihn sorgfältig.",
"team_inviteLinkErrorName": "Bitte gib einen Namen für die eingeladene Person ein. Er kann später geändert werden. ",
"team_inviteLinkCreate": "Link erstellen",
"team_inviteLinkCopy": "Link kopieren",
@ -1500,5 +1500,83 @@
"og_register": "Registriere einen Account auf {0}",
"admin_conflictExplanation": "Es gibt zwei Versionen dieses Dokuments. Wenn du die archivierte Version wiederherstellst, wird die aktuelle Version überschrieben. Wenn du die aktuelle Version archivierst, überschreibst du die archivierte Version. Beide Aktionen können nicht rückgängig gemacht werden.",
"admin_note": "Abo-Notiz",
"admin_planName": "Abo-Name"
"admin_planName": "Abo-Name",
"calendar_rec_until_count2": "Ereignissen",
"calendar_rec_until_date": "Am",
"calendar_rec_until_no": "Nie",
"calendar_rec_until": "Wiederholen beenden",
"calendar_str_filter_month": "Monate: {0}",
"calendar_str_filter_weekno": "Wochen: {0}",
"calendar_str_filter_day": "Tage: {0}",
"calendar_rec_edit": "Dies ist ein sich wiederholendes Ereignis",
"calendar_rec_edit_one": "Nur dieses Ereignis bearbeiten",
"calendar_rec_edit_from": "Zukünftige Ereignisse bearbeiten",
"calendar_rec_edit_all": "Alle Ereignisse bearbeiten",
"calendar_rec_stop": "Nicht mehr wiederholen",
"calendar_month_last": "letzter Tag",
"calendar_rec_until_count": "Nach",
"calendar_rec_freq_yearly": "Jahre",
"calendar_rec_freq_monthly": "Monate",
"calendar_rec_freq_weekly": "Wochen",
"calendar_rec_freq_daily": "Tage",
"calendar_str_filter": "Filter:",
"calendar_rec_txt": "Wiederholen alle",
"calendar_rec_custom": "Benutzerdefiniert",
"calendar_rec_weekdays": "Täglich an Wochentagen",
"calendar_rec_weekend": "Täglich an Wochenenden",
"calendar_rec_weekly": "Wöchentlich am {0}",
"calendar_rec_daily": "Täglich",
"calendar_rec_no": "Einmalig",
"calendar_rec": "Wiederholen",
"fm_rmFilter": "Filter entfernen",
"fm_filterBy": "Filter",
"calendar_rec_yearly": "Jährlich am {2}",
"calendar_str_yearly": "{0} Jahr(e)",
"calendar_str_monthly": "{0} Monat(e)",
"calendar_nth_5": "fünften",
"calendar_rec_every_date": "Jeden {0}",
"calendar_nth_4": "vierten",
"calendar_nth_3": "dritten",
"calendar_list": "{0}, {1}",
"calendar_nth_2": "zweiten",
"calendar_list_end": "{0} oder {1}",
"calendar_nth_1": "ersten",
"calendar_rec_monthly": "Monatlich, Tag {1}",
"calendar_str_for": "für {0} Ereignisse",
"calendar_str_until": "bis zum {0}",
"calendar_str_monthday": "am {0}",
"calendar_rec_monthly_nth": "Jeden {0} {1} im Monat",
"calendar_rec_yearly_nth": "Jeden {0} {1} im {2}",
"calendar_rec_monthly_pick": "An Tagen",
"calendar_str_daily": "{0} Tag(e)",
"calendar_str_weekly": "{0} Woche(n)",
"calendar_str_nthdayofmonth": "am {0} im {1}",
"calendar_str_day": "am {0}",
"calendar_nth_last": "letzten",
"calendar_str_filter_monthday": "Tage im Monat: {0}",
"calendar_str_filter_yearday": "Tage im Jahr: {0}",
"calendar_rec_updated": "Regel aktualisiert am {0}",
"calendar_rec_warn_delall": "Dieses Ereignis wird nicht mehr wiederholt. Das erste Ereignis am {0} bleibt unverändert, alle anderen werden entfernt.",
"calendar_rec_warn_del": "Dieses Ereignis wird nicht mehr wiederholt. Zukünftige Ereignisse werden entfernt.",
"calendar_removeNotification": "Erinnerung entfernen",
"calendar_rec_warn_updateall": "Die Regel für die Wiederholung dieses Ereignisses wurde geändert. Das erste Ereignis am {0} bleibt unverändert, alle anderen werden ersetzt.",
"calendar_rec_warn_update": "Die Regel für die Wiederholung dieses Ereignisses wurde geändert. Zukünftige Ereignisse werden ersetzt.",
"form_anonymized": "Antworten werden anonymisiert",
"form_editable_str": "Absenden",
"form_answer_new": "Erneut absenden",
"form_exportJSON": "Als JSON exportieren",
"form_deleteAll": "Alle löschen",
"form_responseNotification": "Neue Antworten für das Formular: <b>{0}</b>",
"form_alreadyAnsweredMult": "Du hast dieses Formular beantwortet am:",
"form_editable_on_del": "Einmalig und Bearbeiten/Löschen",
"form_multiple_edit": "Mehrmals und Bearbeiten/Löschen",
"form_multiple": "Mehrmals",
"form_editable_off": "Einmalig",
"form_allowNotifications": "Benachrichtigungen über neue Antworten",
"team_inviteRole": "Erste Rolle",
"team_linkUsesInfinite": "(unbegrenzte Verwendungen)",
"team_inviteUses": "Erlaubte Verwendung(en) dieses Links (0 = keine Begrenzung)",
"team_linkUses": "({0}/{1} verbleibend)",
"form_settingsButton": "Formulareinstellungen",
"form_editable_on": "Einmalig und Bearbeiten"
}

View File

@ -138,7 +138,7 @@
"fm_newFile": "Nuevo documento",
"fm_type": "Tipo",
"fm_categoryError": "No se pudo abrir la categoría seleccionada, mostrando la raíz.",
"settings_userFeedbackHint1": "CryptPad suministra informaciones muy básicas al servidor, para ayudarnos a mejorar vuestra experiencia.",
"settings_userFeedbackHint1": "CryptPad suministra informaciones muy básicas al servidor, para ayudarnos a mejorar tu experiencia. ",
"settings_userFeedbackHint2": "El contenido de tus documentos nunca será compartido con el servidor.",
"settings_userFeedback": "Activar feedback",
"settings_anonymous": "No has iniciado sesión. Tus ajustes se aplicarán sólo a este navegador.",
@ -322,7 +322,7 @@
"tags_add": "Actualizar las etiquetas para los documentos seleccionados",
"tags_notShared": "Tus etiquetas no están compartidas con otros usuarios",
"tags_duplicate": "Duplicar etiquetas: {0}",
"tags_noentry": "No puedes etiquetar un documento eliminado!",
"tags_noentry": "¡No puedes etiquetar un documento eliminado!",
"slide_invalidLess": "Estilo personalizado no válido",
"ok": "OK",
"show_help_button": "Mostrar ayuda",
@ -1400,7 +1400,7 @@
"support_warning_document": "Por favor especifica que tipo de documento está causando el problema y provee un <a>identificador de documento</a> o un link",
"support_warning_drives": "Ten en cuenta que los/as administradores/as no pueden identificar carpetas y documentos por nombre. Para carpetas compartidas, por favor provee un <a>identificador de documento</a>",
"support_warning_account": "Por favor ten en cuenta que los/as administradores/as no pueden restaurar contraseñas. Si has perdido las credenciales para tu cuenta pero sigues con la sesión iniciada, puedes <a>migrar tus datos a una nueva cuenta</a>",
"support_warning_prompt": "Por favor escoge la categoría más relevante para tu problema. Esto ayuda a los/as administradores/as a triajar y proveer más sugerencias acerca de que información proveer.",
"support_warning_prompt": "Por favor, elige la categoría más relevante para tu problema. Esto ayuda a los administradores a clasificar y proporcionar más sugerencias sobre qué información proporcionar",
"info_sourceFlavour": "<a>Código fuente</a> para CryptPad",
"info_termsFlavour": "<a>Términos de servicio</a> para esta instancia",
"footer_source": "Código fuente",
@ -1500,5 +1500,65 @@
"admin_listMyInstanceHint": "Si tu instancia es adecuada para el uso público puedes consentir a que sea enlistada en los directorios de la red. La telemetría del servidor debe estar activada para que esto tenga algún efecto.",
"admin_listMyInstanceTitle": "Listar mi instancia en los directorios públicos",
"admin_consentToContactLabel": "Consiento",
"admin_consentToContactHint": "La telemetría del servidor incluye el correo de contacto del administrador/a para que así los/as desarrolladores/as puedan notificarte de problemas serios con el software o tu configuración. Nunca será compartido, vendido, o usado por razones de marketing. Consiente al contacto si te gustaría estar informado/a de problemas críticos en tu servidor."
"admin_consentToContactHint": "La telemetría del servidor incluye el correo de contacto del administrador/a para que así los/as desarrolladores/as puedan notificarte de problemas serios con el software o tu configuración. Nunca será compartido, vendido, o usado por razones de marketing. Consiente al contacto si te gustaría estar informado/a de problemas críticos en tu servidor.",
"calendar_nth_5": "quinto",
"calendar_nth_last": "último",
"calendar_rec_monthly_nth": "Cada {0} {1} del mes",
"calendar_rec_yearly_nth": "Cada {0} {1} de {2}",
"calendar_rec_every_date": "Cada {0}",
"calendar_nth_4": "cuarto",
"calendar_month_last": "último día",
"calendar_nth_3": "tercero",
"calendar_list": "{0}, {1}",
"calendar_nth_2": "segundo",
"calendar_list_end": "{0} ó {1}",
"calendar_nth_1": "primero",
"calendar_str_yearly": "{0} año(s)",
"calendar_str_monthly": "{0} mes(es)",
"calendar_rec_monthly_pick": "En días",
"calendar_str_weekly": "{0} semana(s)",
"calendar_rec_until_count2": "tiempos",
"calendar_rec_until_count": "Después",
"calendar_str_daily": "{0} día(s)",
"calendar_rec_until_date": "En",
"calendar_str_day": "en {0}",
"calendar_rec_until_no": "Nunca",
"calendar_rec_until": "Dejar de repetir",
"calendar_str_monthday": "en el {0}",
"calendar_rec_freq_yearly": "años",
"calendar_str_nthdayofmonth": "en el {0} de {1}",
"calendar_str_for": "por {0} veces",
"calendar_rec_freq_monthly": "meses",
"calendar_str_until": "hasta {0}",
"calendar_rec_freq_weekly": "semanas",
"calendar_rec_freq_daily": "días",
"calendar_str_filter": "Filtros:",
"calendar_rec_txt": "Repetir cada",
"calendar_str_filter_month": "Meses: {0}",
"calendar_str_filter_weekno": "Semanas: {0}",
"calendar_str_filter_yearday": "Días del año: {0}",
"calendar_rec_custom": "Personalizado",
"calendar_str_filter_monthday": "Días del mes: {0}",
"calendar_rec_weekdays": "Diariamente los días de semana",
"calendar_str_filter_day": "Días: {0}",
"calendar_rec_edit": "Este es un evento repetido",
"calendar_rec_weekend": "Diariamente los fines de semana",
"calendar_rec_edit_one": "Solo editar este evento",
"calendar_rec_yearly": "Anualmente en {2}",
"calendar_rec_edit_from": "Editar futuros eventos",
"calendar_rec_monthly": "Mensual, día {1}",
"calendar_rec_edit_all": "Editar todos los eventos",
"calendar_rec_weekly": "Semanal en {0}",
"calendar_rec_stop": "Dejar de repetir",
"calendar_rec_daily": "Diario",
"calendar_rec_updated": "Regla actualizada en {0}",
"calendar_rec_no": "Una vez",
"calendar_rec": "Repetir",
"fm_rmFilter": "Eliminar filtro",
"fm_filterBy": "Filtro",
"calendar_removeNotification": "Eliminar recordatorio",
"calendar_rec_warn_updateall": "La regla para repetir este evento ha sido modificada. El primer evento en {0} será mantenido, todos los demás serán sustituidos.",
"calendar_rec_warn_update": "La regla para repetir este evento ha sido modificada. Los eventos futuros serán sustituidos.",
"calendar_rec_warn_delall": "Este evento ya no se va a repetir. El primer evento en {0} se mantendrá, todos los demás serán eliminados.",
"calendar_rec_warn_del": "Este evento ya no se va a repetir. Los eventos futuros serán eliminados."
}

View File

@ -1397,7 +1397,7 @@
"admin_enableDiskMeasurementsHint": "Gaituta badago, JSON API amaierako puntu bat agertuko da <code>/api/profiling</code> azpian. Honek diskoko I/O-ren neurketa martxan mantentzen du beheko denbora-leihoan. Ezarpen honek zerbitzariaren errendimenduan eragina izan dezake eta datu sentikorrak ager ditzake. Ezarpen hau desgaituta uztea gomendatzen da zertan ari zaren jakin ezean.",
"admin_enableDiskMeasurementsTitle": "Neurtu diskoaren errendimendua",
"admin_infoNotice2": "Ikus 'Sarea' fitxa xehetasun gehiagorako.",
"admin_infoNotice1": "Erabili hurrengo eremuak zure instantzia deskribatzeko. Informazio hau CryptPad-en etorkizuneko bertsio batean instantziako lehen orrialdean erabiliko da. Zerbitzariaren telemetriaren zati gisa bidaltzen da CryptPad instantzia publikoen zerrendan sartzea aukeratzen baduzu soilik.",
"admin_infoNotice1": "Erabili hurrengo eremuak zure instantzia deskribatzeko. Informazio hau instantziako portadan erabiltzen da. Zerbitzariaren telemetriaren zati gisa ere bidaltzen da CryptPad instantzia publikoen zerrendan sartzea aukeratzen baduzu.",
"admin_reviewCheckupNotice": "<a>Egiaztapen</a>-orria berrikustea gomendatzen da instantzia hau behar bezala konfiguratuta dagoela baieztatzeko.",
"admin_cacheEvictionRequired": "Zerbitzaria ezarpen berriarekin eguneratu da. Mesedez, erabili <b>Flush cachea</b> botoia aldaketa hau erabiltzaile guztientzat ikusgai egongo dela ziurtatzeko.",
"fivehundred_internalServerError": "Barneko zerbitzari-errorea",
@ -1452,7 +1452,7 @@
"admin_pinLogArchived": "Saioa hasteko pin-a artxibatuta dago",
"admin_pinLogAvailable": "Saioa hasteko pin-a eskuragarri dago",
"admin_fileCount": "Fitxategien kopurua",
"admin_channelCount": "Kanalen kopurua",
"admin_channelCount": "Dokumentuen kopurua",
"admin_storageUsage": "Biltegiratutako datuak",
"admin_note": "Planaren oharra",
"admin_planName": "Planaren izena",
@ -1462,14 +1462,14 @@
"admin_blockMetadataPlaceholder": "Blokearen URL absolutua edo erlatiboa",
"admin_blockMetadataHint": "Saioa hasteko blokea CryptPad-en saioa hasteko aukera ematen duen erabiltzaile-izena + pasahitzaren konbinazioa",
"admin_blockMetadataTitle": "Saioa hasteko blokearen informazioa",
"admin_channelArchived": "Kanala artxibatuta dago",
"admin_channelAvailable": "Kanala eskuragarri dago",
"admin_channelArchived": "Artxibatuta",
"admin_channelAvailable": "Eskuragarria",
"admin_currentlyOpen": "Une honetan zabalik",
"admin_documentCreationTime": "Sortze-data",
"admin_documentModifiedTime": "Azken aldaketa",
"admin_documentMetadata": "Uneko metadatuak",
"admin_documentSize": "Dokumentuaren tamaina",
"admin_documentMetadataHint": "Kontsultatu kanal edo fitxategi bat bere id edo URL bidez",
"admin_documentMetadataHint": "Kontsultatu dokumentu edo fitxategi bat bere ID edo URL bidez",
"admin_documentMetadataTitle": "Dokumentuaren informazioa",
"admin_accountMetadataHint": "Sartu erabiltzailearen gako publikoa bere kontuari buruzko datuak eskuratzeko.",
"admin_accountMetadataTitle": "Kontuaren informazioa",
@ -1482,5 +1482,83 @@
"ui_none": "bat ere ez",
"admin_uptimeTitle": "Abiarazte ordua",
"admin_cat_database": "Datu-basea",
"admin_uptimeHint": "Zerbitzaria abiarazi den data eta ordua"
"admin_uptimeHint": "Zerbitzaria abiarazi den data eta ordua",
"admin_documentConflict": "Artxibatu/berreskuratu",
"og_encryptedAppType": "Enkriptatuta {0}",
"ui_jsRequired": "JavaScript gaituta egon behar da zure nabigatzailean enkriptaketak egiteko",
"og_features": "{0} funtzio",
"og_pricing": "{0} prezio",
"og_contact": "{0} kontaktu",
"og_register": "Erregistratu kontu bat {0}-n",
"og_login": "Hasi saioa {0}-n",
"og_default": "CryptPad: muturretik muturrera enkriptatutako lankidetza suitea",
"admin_reportContent": "Eman eduki baten berri",
"admin_lastPinTime": "Azkeneko pinaren jardueraren ordua",
"admin_firstPinTime": "Lehenengo pinaren jardueraren ordua",
"ui_false": "faltsua",
"ui_true": "egia",
"form_anonymous": "Gonbidatuen sarbidea (saioa hasi gabe)",
"calendar_rec_weekly": "Astero {0}-n",
"calendar_rec_stop": "Utzi errepikatzeari",
"calendar_rec_daily": "Egunero",
"calendar_rec_updated": "Araua {0}-n eguneratu da",
"calendar_rec_no": "Behin",
"calendar_rec": "Errepikatu",
"fm_rmFilter": "Kendu iragazkia",
"fm_filterBy": "Iragazkia",
"admin_conflictExplanation": "Dokumentu honen bi bertsio daude. Artxibatutako bertsioa berreskuratzeak uneko bertsioa gainidatziko du. Uneko bertsioa artxibatzeak artxibatutako bertsioa gainidatziko du. Ekintza bat ere ezin da desegin.",
"calendar_rec_until_no": "Inoiz ez",
"calendar_rec_until": "Utzi errepikatzeari",
"calendar_str_monthday": "{0}n",
"calendar_rec_freq_yearly": "urte",
"calendar_str_nthdayofmonth": "{1}ko {0}-n",
"calendar_str_for": "{0} aldiz",
"calendar_rec_freq_monthly": "hilabete",
"calendar_str_until": "{0} arte",
"calendar_rec_freq_weekly": "aste",
"calendar_rec_freq_daily": "egun",
"calendar_str_filter": "Iragazkiak:",
"calendar_rec_txt": "Errepikatu",
"calendar_str_filter_month": "Hilabeteak: {0}",
"calendar_str_filter_weekno": "Asteak: {0}",
"calendar_str_filter_yearday": "Urteko egunak: {0}",
"calendar_rec_custom": "Pertsonalizatua",
"calendar_str_filter_monthday": "Hileko egunak: {0}",
"calendar_rec_weekdays": "Egunero astegunetan",
"calendar_str_filter_day": "Egunak: {0}",
"calendar_rec_edit": "Asteburuetan egunero",
"calendar_rec_weekend": "Asteburuetan egunero",
"calendar_rec_edit_one": "Editatu gertaera hau soilik",
"calendar_rec_yearly": "Urtero {2}-n",
"calendar_rec_edit_from": "Editatu etorkizuneko gertaerak",
"calendar_rec_monthly": "Hilero, {1} eguna",
"calendar_rec_edit_all": "Editatu gertaera guztiak",
"og_teamDrive": "Taldeko diskoa",
"calendar_removeNotification": "Kendu abisua",
"calendar_rec_warn_updateall": "Gertaera hau errepikatzeko araua aldatu zen. {0}-ko lehen gertaera mantenduko da, beste guztiak ordezkatuko dira.",
"calendar_rec_warn_update": "Gertaera hau errepikatzeko araua aldatu zen. Etorkizuneko gertaerak ordezkatuko dira.",
"calendar_rec_warn_delall": "Gertaera hau ez da gehiago errepikatuko. {0}-ko lehen gertaera mantenduko da, beste guztiak kenduko dira.",
"calendar_rec_warn_del": "Gertaera hau ez da gehiago errepikatuko. Etorkizuneko gertaerak kendu egingo dira.",
"calendar_nth_5": "bosgarren",
"calendar_nth_3": "hirugarren",
"calendar_nth_1": "lehenengo",
"calendar_nth_2": "bigarren",
"calendar_nth_4": "laugarren",
"calendar_nth_last": "azken",
"calendar_rec_monthly_nth": "Hilabeteko {0} {1} behin",
"calendar_rec_yearly_nth": "{2}ko {0} {1} behin",
"calendar_rec_every_date": "{0} behin",
"calendar_month_last": "azken eguna",
"calendar_list": "{0}, {1}",
"calendar_list_end": "{0} edo {1}",
"calendar_str_yearly": "{0} urte",
"calendar_str_monthly": "{0} hilabete",
"calendar_rec_monthly_pick": "Egunetan",
"calendar_str_weekly": "{0} aste",
"calendar_rec_until_count2": "aldiz",
"calendar_rec_until_count": "Ondoren",
"calendar_str_daily": "{0} egun",
"calendar_rec_until_date": "Hemen",
"calendar_str_day": "hemen {0}",
"admin_generatedAt": "Denbora-markaren berri eman"
}

View File

@ -0,0 +1 @@
{}

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,13 @@
"pad": "रिच टेक्स्ट",
"code": "कोड",
"poll": "मतदान",
"kanban": "कानबन"
"kanban": "कानबन",
"todo": "टुडू",
"media": "मीडिया",
"file": "फ़ाइल",
"whiteboard": "व्हाइटबोर्ड",
"drive": "क्रिप्टड्राइव",
"slide": "मार्कडाउन स्लाइड्स"
},
"main_title": "क्रिप्टपैड: शून्य ज्ञान, सहयोगात्मक रीयल टाइम संपादन"
}

View File

@ -1495,5 +1495,65 @@
"admin_blockKey": "ブロックの公開鍵",
"admin_blockMetadataPlaceholder": "ブロックの絶対または相対URL",
"admin_restoreReason": "復元の理由を指定し、確認して続行してください",
"admin_archiveReason": "アーカイブの理由を指定し、確認して続行してください"
"admin_archiveReason": "アーカイブの理由を指定し、確認して続行してください",
"calendar_nth_1": "第1",
"calendar_list_end": "{0}または{1}",
"calendar_nth_2": "第2",
"calendar_list": "{0}、{1}",
"calendar_nth_3": "第3",
"calendar_nth_last": "最終",
"calendar_nth_5": "第5",
"calendar_rec_every_date": "毎{0}",
"calendar_nth_4": "第4",
"calendar_month_last": "最終日",
"calendar_str_yearly": "{0}年",
"calendar_str_weekly": "{0}週",
"calendar_str_monthly": "{0}月",
"calendar_rec_until_count2": "回",
"calendar_str_daily": "{0}日",
"calendar_str_filter_month": "月:{0}",
"calendar_str_filter_weekno": "週:{0}",
"calendar_rec_custom": "ユーザー定義",
"calendar_rec_edit": "これは繰り返すイベントです",
"calendar_rec_weekend": "毎週末",
"calendar_rec_edit_one": "このイベントのみを編集",
"calendar_rec_yearly": "毎年{2}",
"calendar_rec_edit_from": "今後のイベントを編集",
"calendar_rec_monthly": "毎月{1}日",
"calendar_rec_edit_all": "全てのイベントを編集",
"calendar_rec_weekly": "毎週{0}",
"calendar_rec_stop": "繰り返しを停止",
"calendar_rec_daily": "毎日",
"calendar_rec_until": "繰り返しを停止",
"calendar_str_monthday": "{0}に",
"calendar_rec_freq_yearly": "年",
"calendar_str_for": "{0}回まで",
"calendar_rec_freq_monthly": "月",
"calendar_str_until": "{0}まで",
"calendar_rec_freq_weekly": "週",
"calendar_rec_freq_daily": "日",
"calendar_str_filter": "フィルター:",
"calendar_rec_txt": "繰り返しの頻度",
"calendar_rec_no": "一度",
"calendar_rec": "繰り返す",
"fm_rmFilter": "フィルターを削除",
"fm_filterBy": "フィルター",
"admin_conflictExplanation": "このドキュメントには2つのバージョンがあります。アーカイブされたバージョンを復元すると、現在のバージョンが上書きされます。現在のバージョンをアーカイブすると、アーカイブ済のバージョンが上書きされます。どちらのアクションも取り消しできません。",
"admin_documentConflict": "アーカイブ/復元",
"calendar_rec_until_count": "以後",
"form_editable_str": "送信",
"form_multiple": "複数回",
"form_editable_off": "一度のみ",
"form_allowNotifications": "新しい回答の通知",
"form_answer_new": "再送信",
"form_deleteAll": "全て削除",
"form_exportJSON": "JSONでエクスポート",
"team_inviteRole": "初期の役割",
"calendar_rec_warn_delall": "この予定は繰り返しません。{0}の予定を残し、それ以外は削除されます。",
"calendar_rec_warn_del": "この予定は今後繰り返しません。将来の予定が削除されます。",
"calendar_rec_weekdays": "平日全て",
"calendar_str_filter_day": "日:{0}",
"calendar_rec_warn_update": "このイベントの繰り返しに関するルールが変更されました。 今後のイベントは置き換えられます。",
"calendar_rec_warn_updateall": "このイベントの繰り返しに関するルールが変更されました。 {0}の最初のイベントは維持されます。他の全ては置き換えられます。",
"calendar_removeNotification": "リマインダーを削除"
}

View File

@ -833,7 +833,7 @@
"team_inviteLinkNote": "Add a personal message",
"team_inviteLinkNoteMsg": "This message will be shown before the recipient decides whether to join this team.",
"team_inviteLinkLoading": "Generating your link",
"team_inviteLinkWarning": "The first person to access this link will be able to join this team and view its contents. Share it carefully.",
"team_inviteLinkWarning": "People who access this link will be able to join this team and view its contents. Share it carefully.",
"team_inviteLinkErrorName": "Please add a name for the person you're inviting. They can change it later. ",
"team_inviteLinkCreate": "Create link",
"team_inviteLinkCopy": "Copy link",
@ -1500,5 +1500,83 @@
"ui_jsRequired": "JavaScript must be enabled to perform encryption in your browser",
"og_encryptedAppType": "Encrypted {0}",
"admin_documentConflict": "Archive/restore",
"admin_conflictExplanation": "Two versions of this document exist. Restoring the archived version will overwrite the live version. Archiving the live version will overwrite the archived version. Neither action can be undone."
"admin_conflictExplanation": "Two versions of this document exist. Restoring the archived version will overwrite the live version. Archiving the live version will overwrite the archived version. Neither action can be undone.",
"fm_filterBy": "Filter",
"fm_rmFilter": "Remove filter",
"calendar_rec": "Repeat",
"calendar_rec_no": "One time",
"calendar_rec_updated": "Rule updated on {0}",
"calendar_rec_daily": "Daily",
"calendar_rec_stop": "Stop repeating",
"calendar_rec_weekly": "Weekly on {0}",
"calendar_rec_edit_all": "Edit all events",
"calendar_rec_monthly": "Monthly, day {1}",
"calendar_rec_edit_from": "Edit future events",
"calendar_rec_yearly": "Yearly on {2}",
"calendar_rec_edit_one": "Edit only this event",
"calendar_rec_weekend": "Daily on weekends",
"calendar_rec_edit": "This is a repeating event",
"calendar_str_filter_day": "Days: {0}",
"calendar_rec_weekdays": "Daily on weekdays",
"calendar_str_filter_monthday": "Days of month: {0}",
"calendar_rec_custom": "Custom",
"calendar_str_filter_yearday": "Days of year: {0}",
"calendar_str_filter_weekno": "Weeks: {0}",
"calendar_str_filter_month": "Months: {0}",
"calendar_rec_txt": "Repeat every",
"calendar_str_filter": "Filters:",
"calendar_rec_freq_daily": "days",
"calendar_rec_freq_weekly": "weeks",
"calendar_str_until": "until {0}",
"calendar_rec_freq_monthly": "months",
"calendar_str_for": "for {0} times",
"calendar_str_nthdayofmonth": "on the {0} of {1}",
"calendar_rec_freq_yearly": "years",
"calendar_str_monthday": "on the {0}",
"calendar_rec_until": "Stop Repeating",
"calendar_rec_until_no": "Never",
"calendar_str_day": "on {0}",
"calendar_rec_until_date": "On",
"calendar_str_daily": "{0} day(s)",
"calendar_rec_until_count": "After",
"calendar_rec_until_count2": "times",
"calendar_str_weekly": "{0} week(s)",
"calendar_rec_monthly_pick": "On days",
"calendar_str_monthly": "{0} month(s)",
"calendar_str_yearly": "{0} year(s)",
"calendar_nth_1": "first",
"calendar_list_end": "{0} or {1}",
"calendar_nth_2": "second",
"calendar_list": "{0}, {1}",
"calendar_nth_3": "third",
"calendar_month_last": "last day",
"calendar_nth_4": "fourth",
"calendar_rec_every_date": "Every {0}",
"calendar_rec_yearly_nth": "Every {0} {1} of {2}",
"calendar_rec_monthly_nth": "Every {0} {1} of the month",
"calendar_nth_last": "last",
"calendar_nth_5": "fifth",
"calendar_rec_warn_del": "This event will no longer repeat. Future events will be removed.",
"calendar_rec_warn_delall": "This event will no longer repeat. The first event on {0} will be kept, all others will be removed.",
"calendar_rec_warn_update": "The rule for repeating this event was modified. Future events will be replaced.",
"calendar_rec_warn_updateall": "The rule for repeating this event was modified. The first event on {0} will be kept, all others will be replaced.",
"calendar_removeNotification": "Remove reminder",
"team_inviteRole": "Initial role",
"team_inviteUses": "Use(s) allowed for this link (0 = no limit)",
"form_exportJSON": "Export to JSON",
"form_alreadyAnsweredMult": "You responded to this form on:",
"form_responseNotification": "New responses to form: <b>{0}</b>",
"form_deleteAll": "Delete all",
"form_answer_new": "Submit again",
"form_allowNotifications": "Notifications for new responses",
"form_editable_off": "One time only",
"form_editable_on_del": "One time and edit/delete",
"form_multiple": "Multiple times",
"form_multiple_edit": "Multiple times and edit/delete",
"form_editable_str": "Submission",
"team_linkUsesInfinite": "(unlimited uses)",
"team_linkUses": "({0}/{1} remaining)",
"form_anonymized": "Responses are anonymized",
"form_settingsButton": "Form settings",
"form_editable_on": "One time and edit"
}

View File

@ -180,7 +180,7 @@
"contacts_confirmRemove": "Czy na pewno chcesz usunąć <em>{0}</em> ze swoich kontaktów?",
"contacts_remove": "Usuń ten kontakt",
"contacts_send": "Wyślij",
"contacts_request": "<em>{0}</em> chciałby dodać Cię jako kontakt. <b>Akceptujesz</b>?",
"contacts_request": "<em>{0}</em> chciał(a)by dodać Cię jako kontakt. <b>Akceptujesz</b>?",
"contacts_rejected": "Zaproszenie do kontaktu odrzucone",
"contacts_added": "Zaproszenie do kontaktu przyjęte.",
"contacts_title": "Kontakty",
@ -1397,5 +1397,6 @@
"admin_descriptionTitle": "Opis istancji",
"ui_saved": "{0} zapisano",
"admin_nameHint": "Wyświetlana nazwa dla tego urządzenia z listy publicznych instacji na stronie cryptpad.org",
"admin_archiveNote": "Notatka"
"admin_archiveNote": "Notatka",
"common_connectionLost": "<b>Utracono połączenie z serwerem</b><br>Dopóki połączenie nie wróci, włączony będzie tryb tylko do odczytu."
}

View File

@ -88,14 +88,14 @@
"newButton": "Создать",
"uploadButton": "Загрузить файлы",
"uploadButtonTitle": "Загрузить новый файл в ваш CryptDrive",
"saveTemplateButton": "Сохранить как образец",
"saveTemplatePrompt": "Выбрать название для образца",
"templateSaved": "Образец сохранен!",
"selectTemplate": "Выберите образец или нажмите Esc",
"useTemplate": "Начать с образца?",
"useTemplateOK": "Выбрать образец (Enter)",
"template_import": "Импортировать образец",
"template_empty": "Образцы отсутствуют",
"saveTemplateButton": "Сохранить как шаблон",
"saveTemplatePrompt": "Выберите название для шаблона",
"templateSaved": "Шаблон сохранен!",
"selectTemplate": "Выберите шаблон или нажмите Esc",
"useTemplate": "Использовать шаблон?",
"useTemplateOK": "Выбрать шаблон (Enter)",
"template_import": "Импортировать шаблон",
"template_empty": "Шаблоны отсутствуют",
"presentButtonTitle": "Начать режим презентации",
"backgroundButtonTitle": "Изменить фоновый цвет в презентации",
"colorButtonTitle": "Изменить цвет шрифта в презентации",
@ -357,7 +357,7 @@
"settings_exportCancel": "Вы уверены, что хотите отменить экспорт? В следующий раз вам придется начинать все сначала.",
"settings_export_reading": "Читаем ваше хранилище...",
"settings_export_download": "Скачиваем и расшифровываем ваши документы...",
"contacts_request": "<em>{0}</em> хотел бы добавить вас в список контактов. <b>Принять </b>?",
"contacts_request": "<em>{0}</em> хотел(а) бы добавить Вас в свой список контактов. <b>Согласны</b>?",
"contacts_confirmRemove": "Вы уверены, что хотите удалить <em>1{0}</em>2 из ваших контактов?",
"register_acceptTerms": "Я принимаю <a>условия использования</a>",
"register_warning": "Внимание",
@ -412,7 +412,7 @@
"burnAfterReading_generateLink": "Нажмите на кнопку ниже, чтобы создать ссылку.",
"upload_size": "Размер",
"upload_pending": "Ожидайте",
"upload_tooLargeBrief": "Размер файла превышает лимит в {0}МБ",
"upload_tooLargeBrief": "Размер файла превышает лимит в {0}МБ установленный на этом Диске",
"upload_notEnoughSpaceBrief": "Недостаточно места",
"upload_notEnoughSpace": "Недостаточно места для этого файла на вашем CryptDrive.",
"settings_cursorShareTitle": "Делиться позицией моего курсора",
@ -745,7 +745,7 @@
"feedback_optout": "Если вы хотите отказаться, посетите <a>страницу настроек пользователя</a>, где вы найдете флажок для включения или отключения обратной связи с пользователем.",
"feedback_privacy": "Мы заботимся о Вашей конфиденциальности и в то же время хотим, чтобы CryptPad был очень простым в использовании. Мы используем этот файл, чтобы выяснить, какие функции пользовательского интерфейса важны для наших пользователей, запрашивая его вместе с параметром, указывающим, какое действие было предпринято.",
"feedback_about": "Если Вы читаете это, Вам, вероятно, было любопытно, почему CryptPad запрашивает веб-страницы при выполнении определенных действий.",
"help_genericMore": "Узнайте больше о том, как CryptPad может работать для вас, прочитав нашу <a>Документацию</a>.",
"help_genericMore": "Узнайте больше о том, как CryptPad может работать для вас, прочитав нашу <a>Документацию</a>",
"header_logoTitle": "Перейти в Ваш CryptDrive",
"features_f_cryptdrive0_note": "Возможность сохранять в вашем браузере посещенные документы, чтобы иметь возможность открывать их позже",
"settings_padOpenLinkLabel": "Разрешить открытие прямой ссылки",
@ -760,7 +760,7 @@
"team_inviteLinkCopy": "Копировать ссылку",
"team_inviteLinkCreate": "Создать ссылку",
"team_inviteLinkErrorName": "Добавьте имя человека, которого Вы приглашаете. Они могут изменить это позже. ",
"team_inviteLinkWarning": "Первый, кто получит доступ к этой ссылке, сможет присоединиться к этой команде и просмотреть её содержимое. Делитесь ею с осторожностью.",
"team_inviteLinkWarning": "Те, кто получат к эту ссылку, смогут присоединиться к этой команде и просмотреть её содержимое. Делитесь ссылкой с осторожностью.",
"team_inviteLinkLoading": "Создание Вашей ссылки",
"team_inviteLinkNoteMsg": "Это сообщение будет показано до того, как получатель решит, присоединиться ли к этой команде.",
"team_inviteLinkNote": "Добавьте личное сообщение",
@ -939,7 +939,7 @@
"admin_authError": "Только администраторы могут получить доступ к этой странице",
"survey": "Опрос CryptPad",
"crowdfunding_popup_text": "<h3>Нам нужна ваша помощь!</h3>Чтобы быть уверенными, что CryptPad активно развивается - рассмотрите возможность поддержки проекта через страницу OpenCollective, где Вы можете увидеть нашу <b>Дорожную карту</b> и<b>Цели финансирования</b>.",
"crowdfunding_button2": "Помочь CryptPad",
"crowdfunding_button2": "Помочь деньгами",
"autostore_notAvailable": "Вы должны сохранить этот документ на Вашем CryptDrive, прежде чем сможете использовать эту функцию.",
"autostore_forceSave": "Сохраните файл в Вашем CryptDrive",
"autostore_saved": "Документ успешно сохранен на вашем CryptDrive!",
@ -1166,9 +1166,9 @@
"support_cat_bug": "Сообщить об ошибке",
"support_cat_data": "Пропажа содержимого",
"support_cat_account": "Учетная запись пользователя",
"info_privacyFlavour": "Наша <a>политика конфиденциальности</a> описывает, как мы обрабатываем Ваши данные.",
"info_privacyFlavour": "<a>Политика конфиденциальности</a> этого экземпляра CryptPad",
"user_about": "О CryptPad",
"info_imprintFlavour": "<a>Правовая информация об администраторах данного экземпляра</a>.",
"info_imprintFlavour": "<a>Правовая информация</a> об администраторах данного экземпляра",
"settings_safeLinkDefault": "Безопасные ссылки теперь включены по умолчанию. Для копирования ссылок используйте меню <i></i><b>Поделиться</b>, а не адресную строку браузера.",
"slide_textCol": "Цвет текста",
"slide_backCol": "Цвет фона",
@ -1386,6 +1386,196 @@
"bounce_confirm": "Вы покинете: {0}\n\nВы точно хотите перейти к \"{1}\"?",
"ui_restore": "Восстановить",
"ui_archive": "Архивировать",
"ui_undefined": "неизвестный",
"admin_documentType": "Тип"
"ui_undefined": "неизвестно",
"admin_documentType": "Тип документа",
"form_settingsButton": "Настройки формы",
"form_anonymized": "Ответы анонимизированы",
"team_linkUses": "({0}/{1} осталось)",
"team_linkUsesInfinite": "(неограниченное использование)",
"form_editable_str": "Подача",
"form_multiple_edit": "Многократно и редактировать/удалять",
"form_multiple": "Многократно",
"form_editable_on_del": "Один раз и отредактировать/удалить",
"form_editable_off": "Только один раз",
"form_allowNotifications": "Уведомления о новых ответах",
"form_answer_new": "Отправить снова",
"form_deleteAll": "Удалить все",
"form_responseNotification": "Новые ответы на форму: <b>{0}</b>",
"form_alreadyAnsweredMult": "Вы ответили на эту форму:",
"form_exportJSON": "Экспорт в JSON",
"team_inviteUses": "Сколько раз можно использовать эту ссылку (0 = без ограничений)",
"team_inviteRole": "Начальная роль",
"calendar_removeNotification": "Удалить напоминание",
"calendar_rec_warn_updateall": "Правило для повторения этого события было изменено. Первое событие на {0} будет сохранено, все остальные будут заменены.",
"calendar_rec_warn_update": "Правило для повторения этого события было изменено. Будущие события будут заменены.",
"calendar_rec_warn_delall": "Это событие больше не повторится. Первое событие на {0} будет сохранено, все остальные будут удалены.",
"calendar_rec_warn_del": "Это событие больше не повторится. Будущие события будут удалены.",
"calendar_nth_5": "пятый",
"calendar_nth_last": "последний",
"calendar_rec_monthly_nth": "Каждый {0} {1} месяца",
"calendar_rec_yearly_nth": "Каждый {0} {1} {2}",
"calendar_rec_every_date": "Каждый {0}",
"calendar_nth_4": "четвёртый",
"calendar_month_last": "последний день",
"calendar_nth_3": "третий",
"calendar_list": "{0}, {1}",
"calendar_nth_2": "второй",
"calendar_list_end": "{0} или {1}",
"calendar_nth_1": "первый",
"calendar_str_yearly": "{0} год(ы)",
"calendar_str_monthly": "{0} месяц(ы)",
"calendar_rec_monthly_pick": "По дням",
"calendar_str_weekly": "{0} недель",
"calendar_rec_until_count2": "раз",
"calendar_rec_until_count": "После",
"calendar_str_daily": "{0} день",
"calendar_rec_until_date": "На",
"calendar_str_day": "на {0}",
"calendar_rec_until_no": "Никогда",
"calendar_rec_until": "Прекратить повторение",
"calendar_str_monthday": "на {0}",
"calendar_rec_freq_yearly": "годы",
"calendar_str_nthdayofmonth": "на {0} день {1}",
"calendar_str_for": "{0} раз",
"calendar_rec_freq_monthly": "месяцы",
"calendar_str_until": "до {0}",
"calendar_rec_freq_weekly": "недели",
"calendar_rec_freq_daily": "дни",
"calendar_str_filter": "Фильтры:",
"calendar_rec_txt": "Повторять каждый",
"calendar_str_filter_month": "Месяцы: {0}",
"calendar_str_filter_weekno": "Недели: {0}",
"calendar_str_filter_yearday": "Дни года: {0}",
"calendar_rec_custom": "Пользовательское",
"calendar_str_filter_monthday": "Дни месяца: {0}",
"calendar_rec_weekdays": "Ежедневно по будням",
"calendar_str_filter_day": "Дни: {0}",
"calendar_rec_edit": "Это повторяющееся событие",
"calendar_rec_weekend": "Ежедневно по выходным",
"calendar_rec_edit_one": "Редактировать только это событие",
"calendar_rec_yearly": "Ежегодно по {2}",
"calendar_rec_edit_from": "Редактировать будущие события",
"calendar_rec_monthly": "Ежемесячно, день {1}",
"calendar_rec_edit_all": "Редактировать все события",
"calendar_rec_weekly": "Еженедельно по {0}",
"calendar_rec_stop": "Прекратить повторение",
"calendar_rec_daily": "Ежедневно",
"calendar_rec_updated": "Правило обновлено {0}",
"calendar_rec_no": "Один раз",
"calendar_rec": "Повтор",
"fm_rmFilter": "Удалить фильтр",
"fm_filterBy": "Фильтр",
"admin_conflictExplanation": "Существуют две версии этого документа. Восстановление архивной версии приведет к перезаписи текущей версии. Архивирование текущей версии приведет к перезаписи архивной версии. Ни одно из действий не может быть отменено.",
"admin_documentConflict": "Архивировать/восстановить",
"og_encryptedAppType": "Зашифровано {0}",
"ui_jsRequired": "Для выполнения шифрования в вашем браузере должен быть включен JavaScript",
"og_features": "{0} Возможности",
"og_pricing": "{0} Цены",
"og_contact": "{0} Контакт",
"og_register": "Создать учетную запись на {0}",
"og_login": "Войти в {0}",
"og_default": "CryptPad: пакет для совместной работы со сквозным шифрованием",
"og_teamDrive": "Диск команды",
"admin_getRawMetadata": "История метаданных",
"admin_planlimit": "Лимит хранилища",
"admin_restoreDocument": "Восстановить документ из архива",
"admin_archiveDocument": "Поместить документ в архив",
"admin_restoreArchivedPins": "Восстановить журнал пин-кодов из архива",
"admin_archivePinLog": "Архивировать лог пинов этой учетной записи",
"admin_getPinList": "Текущий список пинов",
"admin_restoreBlock": "Восстановить блок из архива",
"admin_archiveBlock": "Архивировать блок",
"admin_blockArchived": "Блок помещён в архив",
"admin_blockAvailable": "Блок доступен",
"admin_blockKey": "Публичный ключ блока",
"admin_pinLogArchived": "Журнал пинов находится в архиве",
"admin_pinLogAvailable": "Журнал пин-кодов доступен",
"admin_fileCount": "Количество файлов",
"admin_channelCount": "Количество документов",
"admin_storageUsage": "Размер данных",
"admin_note": "Примечание к тарифному плану",
"admin_planName": "Название тарифного плана",
"admin_currentlyOnline": "В настоящее время онлайн",
"admin_lastPinTime": "Время активности второго PIN-а",
"admin_firstPinTime": "Время активности первого PIN-а",
"admin_accountMetadataPlaceholder": "Идентификатор пользователя (открытый ключ подписи)",
"admin_blockMetadataPlaceholder": "Абсолютный или относительный URL-адрес блока",
"admin_blockMetadataHint": "Блок информации о логине — это то, что позволяет войти в CryptPad под своей учётной записью с помощью комбинации имени пользователя и пароля",
"admin_blockMetadataTitle": "Блок информации о логине",
"admin_documentMetadataPlaceholder": "URL-адрес или идентификатор документа",
"admin_channelArchived": "Архивировано",
"admin_channelAvailable": "Доступно",
"admin_currentlyOpen": "Сейчас открыто",
"admin_documentModifiedTime": "Последнее изменение",
"admin_documentCreationTime": "Создано",
"admin_documentMetadata": "Метаданные на данный момент",
"admin_documentSize": "Размер документа",
"admin_documentMetadataHint": "Запросить документ или файл по его идентификатору или URL-адресу",
"admin_documentMetadataTitle": "Информация о документе",
"admin_accountMetadataHint": "Введите открытый ключ пользователя, чтобы получить данные об его учетной записи.",
"admin_accountMetadataTitle": "Информация об учетной записи",
"admin_restoreReason": "Пожалуйста, укажите причину восстановления и подтвердите, что Вы хотели бы продолжить",
"admin_archiveReason": "Пожалуйста, укажите причину архивации и подтвердите, что Вы хотели бы продолжить",
"ui_confirm": "Подтвердить",
"ui_fetch": "Загрузить (fetch)",
"ui_success": "Успешно",
"ui_generateReport": "Создание отчета",
"ui_none": "нет значения",
"ui_false": "нет",
"ui_true": "да",
"admin_generatedAt": "Дата и время отчёта",
"admin_cat_database": "База данных",
"admin_uptimeHint": "Дата и время, в которое был запущен сервер",
"admin_uptimeTitle": "Время запуска",
"register_instance": "Создание новой учетной записи на {0}",
"login_instance": "Подключитесь к своей учетной записи на {0}",
"home_morestorage": "Чтобы получить больше места:",
"home_location": "Зашифрованные данные размещены в {0}",
"footer_website": "Веб-сайт проекта",
"admin_noticeHint": "Необязательное сообщение для отображения на главной странице",
"admin_noticeTitle": "Уведомление на домашней странице",
"ui_experimental": "Эта функция считается экспериментальной.",
"error_evalPermitted": "Прервано, потому что eval не должно быть разрешено.\n\nЭта ошибка связана с заголовками политики безопасности содержимого (Content-Security-Policy headers), это может быть связано с: устаревшим браузером, который их не поддерживает, расширениями браузера, которые мешают их правильному поведению, или неправильной конфигурацией этого экземпляра CryptPad.",
"error_incorrectAccess": "Доступ к этой странице возможен только через {0}.",
"error_embeddingDisabledSpecific": "Встраивание отключено для этого приложения CryptPad.",
"error_embeddingDisabled": "Встраивание отключено для этого экземпляра CryptPad",
"admin_enableembedsHint": "Разрешить встраивать документы и носители из этого экземпляра на другие веб-сайты. Это добавит опцию 'Встроить' в меню 'Поделиться'. По соображениям безопасности приложения, использующие OnlyOffice (Листы, Документ, Презентация), не могут быть встроены, даже если этот параметр активен.",
"admin_enableembedsTitle": "Включить удаленное встраивание",
"ui_ms": "миллисекунд",
"admin_setDuration": "Установить продолжительность",
"admin_bytesWrittenHint": "Если Вы включили измерение производительности диска, то продолжительность окна можно настроить ниже.",
"admin_bytesWrittenTitle": "Окно измерения производительности диска",
"admin_enableDiskMeasurementsHint": "Если включено, эндпойнт JSON API будет доступен в разделе <code>/api/profiling</code>. Это позволяет поддерживать текущее измерение дискового ввода-вывода в пределах временного окна, установленного ниже. Этот параметр может повлиять на производительность сервера и может привести к раскрытию конфиденциальных данных. Рекомендуется оставить этот параметр отключенным, если только Вы не знаете, что делаете.",
"admin_enableDiskMeasurementsTitle": "Измерение производительности диска",
"admin_infoNotice2": "Подробности смотрите на вкладке 'Сеть'.",
"admin_infoNotice1": "Используйте следующие поля для описания Вашего экземпляра. Эта информация используется на главной странице экземпляра. Она также отправляется как часть телеметрии сервера, если Вы хотите быть включенным в список общедоступных экземпляров CryptPad.",
"admin_reviewCheckupNotice": "Рекомендуется просмотреть страницу <a>проверки</a>, чтобы убедиться, что этот экземпляр настроен правильно.",
"admin_cacheEvictionRequired": "Сервер был обновлен с учетом новых настроек. Пожалуйста, используйте кнопку <b>Очистить кэш</b>, чтобы убедиться, что это изменение станет видимым для всех пользователей.",
"fivehundred_internalServerError": "Внутренняя ошибка сервера",
"support_debuggingDataHint": "Следующая информация будет включена в отправленные Вами обращения в службу поддержки. Ничто из этого не позволяет администраторам получать доступ к Вашим документам или расшифровывать их. Эта информация зашифрована таким образом, что только администраторы могут ее прочитать.",
"support_debuggingDataTitle": "Отладочные данные учётной записи",
"support_cat_debugging": "Отладочные данные",
"ui_openDirectly": "Эта функция недоступна когда (документ) CryptPad встроен в другой сайт. Открыть этот документ на отдельной вкладке браузера?",
"support_cat_abuse": "Нарушены Условия Обслуживания",
"support_cat_document": "Документ",
"support_cat_drives": "Хранилище (Drive) или Команда",
"support_warning_other": "О чём Ваш запрос? Пожалуйста, предоставьте как можно больше актуальной информации, чтобы нам было легче решить вашу проблему быстро",
"support_warning_abuse": "Пожалуйста, сообщайте о контенте, который нарушает <a>Условия Обслуживания</a>. Пожалуйста, предоставьте ссылки на оскорбительные документы или профили пользователей и опишите, как они нарушают условия. Любая дополнительная информация о контексте, в котором Вы обнаружили контент или поведение, может помочь администраторам предотвратить будущие нарушения",
"support_warning_bug": "Пожалуйста, укажите, в каком браузере возникает проблема и установлены ли какие-либо расширения. Пожалуйста, предоставьте как можно больше подробностей о проблеме и шагах, необходимых для ее воспроизведения",
"support_warning_document": "Пожалуйста, укажите, какой тип документа вызывает проблему, и укажите <a>идентификатор документа</a> или ссылку на документ",
"support_warning_drives": "Обратите внимание, что у администраторов нет возможности находить папки и документы по имени. Для общих папок, пожалуйста, укажите <a>идентификатор документа</a>",
"support_warning_account": "Пожалуйста, обратите внимание, что администраторы не могут сменить Ваш пароль. Если Вы потеряли учетные данные для своей учетной записи, но всё ещё авторизованы в системе, Вы можете <a>перенести свои данные в новую учетную запись</a>",
"support_warning_prompt": "Пожалуйста, выберите наиболее подходящую категорию для вашего вопроса. Это помогает администраторам определять срочность и сложность проблемы, и дает дополнительные рекомендации относительно того, какую информацию следует предоставлять",
"info_sourceFlavour": "<a>Исходный код</a> CryptPad",
"info_termsFlavour": "<a>Условия обслуживания</a> в этом экземпляре CryptPad",
"footer_source": "Исходный код",
"admin_jurisdictionHint": "Страна, в которой размещены зашифрованные данные этого экземпляра",
"admin_jurisdictionTitle": "Местоположение хостинга",
"admin_descriptionHint": "Текстовое описание, отображаемое для этого экземпляра в списке общедоступных экземпляров на cryptpad.org",
"admin_descriptionTitle": "Описание экземпляра CryptPad",
"ui_saved": "{0} сохранено",
"admin_nameHint": "Имя, отображаемое для этого экземпляра в списке общедоступных экземпляров на cryptpad.org",
"admin_nameTitle": "Имя экземпляря CryptPad",
"admin_archiveNote": "Заметка",
"form_editable_on": "Один раз и отредактировать"
}

View File

@ -276,7 +276,7 @@
"contacts_confirmRemove": "确定要从联系人中删除 <em>{0}</em> 吗?",
"contacts_remove": "删除此联系人",
"contacts_send": "发送",
"contacts_request": "<em>{0}</em> 想将您添加为联系人。 <b>接受</b>",
"contacts_request": "<em>{0}</em> 想将您添加为联系人。 <b>接受</b>",
"contacts_rejected": "联系人邀请被拒绝",
"contacts_added": "联系人已接受邀请。",
"contacts_title": "联系人",
@ -1414,7 +1414,7 @@
"admin_enableDiskMeasurementsHint": "如果启用JSON API 端点将在 <code>/api/profiling</code> 下公开。 这会在下面设置的时间窗口内保持磁盘 I/O 的运行测量。 此设置可能会影响服务器性能并可能泄露敏感数据。 除非您知道自己在做什幺,否则建议您禁用此设置。",
"admin_enableDiskMeasurementsTitle": "测量磁盘性能",
"admin_infoNotice2": "有关详细信息,请参阅“网络”选项卡。",
"admin_infoNotice1": "使用以下字段来描述您的实例。 此信息将用于 CryptPad 未来版本的实例首页。 仅当您选择加入公共 CryptPad 实例列表时,它才会作为服务器遥测的一部分发送。",
"admin_infoNotice1": "使用以下字段来描述您的实例。 此信息用于 CryptPad 的实例前端页。 如果您选择加入公共 CryptPad 实例列表,它会同时作为服务器遥测的一部分信息发送。",
"admin_reviewCheckupNotice": "建议查看 <a>checkup</a> 页面以确认此实例配置正确。",
"admin_cacheEvictionRequired": "服务器已使用新设置进行了更新。 请使用 <b>刷新缓存</b> 按钮确保此更改对所有用户可见。",
"fivehundred_internalServerError": "内部服务器错误",
@ -1437,5 +1437,9 @@
"ui_restore": "恢复",
"ui_archive": "归档",
"ui_undefined": "未知",
"admin_documentType": "類型"
"admin_documentType": "類型",
"admin_generatedAt": "报告时间戳",
"admin_cat_database": "数据库",
"admin_uptimeHint": "启动服务器的日期和时间",
"admin_uptimeTitle": "启动时间"
}

View File

@ -293,7 +293,7 @@ define([
};
Messages.convertPage = "Convert"; // XXX 4.11.0
Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterward."; // XXX 4.11.0
Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterwards."; // XXX 4.11.0
var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];

View File

@ -193,28 +193,20 @@
display: flex;
justify-content: space-between;
flex-wrap: wrap;
.cp-form-settings-preview {
min-width: 260px;
margin-right: 20px;
}
.cp-form-color-theme-container {
max-width: 300px;
}
& > div {
flex-basis: 33.333333%;
flex: 1;
padding-right: 20px;
}
}
}
}
@media screen and (max-width: 600px) and (min-width: 400px) {
.cp-form-creator-settings {
& > div {
flex-basis: 50% !important;
}
}
}
@media screen and (max-width: 400px) {
.cp-form-creator-settings {
& > div {
flex-basis: 100% !important;
}
}
}
.cp-form-creator-settings {
.cp-form-actions {
margin-top: 5px;
@ -230,6 +222,18 @@
}
}
div.cp-form-settings-preview {
background: @cp_form-bg1;
padding: 10px;
border-radius: @variables_radius_L;
& > *:not(:last-child) {
margin-bottom: 10px;
}
.cp-form-setting-title {
color: @cryptpad_color_link;
}
}
.cp-form-color-container {
& > div {
display: flex;
@ -251,12 +255,6 @@
}
}
}
.cp-form-response-msg-container button {
white-space: initial;
line-height: 25px;
padding: 5.5px 6px;
}
}
div.cp-form-filler-container {
width: 300px;
@ -276,6 +274,7 @@
}
}
div.cp-form-creator-content {
position: relative;
.cp-form-block-type {
margin-top: -35px;
&.editable {
@ -404,7 +403,7 @@
display: flex;
flex-flow: column;
margin: 20px 0px 0px 0px;
padding-bottom: 100px;
padding-bottom: 75px;
&> div:first-child {
display: flex;
height: 100%;
@ -449,6 +448,17 @@
}
}
}
.cp-form-response-msg-container {
padding-bottom: 75px;
button {
white-space: initial;
line-height: 25px;
padding: 5.5px 6px;
}
.cp-form-response-msg-hint {
color: @cryptpad_color_link;
}
}
.cp-form-send-container {
text-align: center;
@ -666,7 +676,7 @@
}
}
&.editable {
cursor: grab;
&:not(.nodrag) { cursor: grab; }
.cp-form-edit-save {
margin-top: 20px;
button {
@ -764,8 +774,13 @@
justify-content: center;
flex: 1;
flex-flow: column;
div.cp-form-submit-table {
display: grid;
grid-gap: 10px;
margin: 10px 0;
}
.cp-form-submit-actions {
button:not(:last-child) {
span:not(:last-child) {
margin-right: 10px;
}
}
@ -833,6 +848,10 @@
background: @cp_form-bg2;
}
.cp-form-results-delete {
vertical-align: top;
}
.cp-form-results-type-multiradio-data {
.cp-mr-q {
font-weight: bold;
@ -915,11 +934,27 @@
display: flex;
flex-flow: column;
align-items: baseline;
.cp-radio {
.cp-radio, .cp-checkmark {
display: inline-flex;
max-width: 100%;
}
.cp-checkmark-label {
word-break: break-word;
}
}
.cp-form-multiradio-container {
overflow: auto;
}
.cp-form-type-multiradio {
.cp-form-multiradio-header {
white-space: nowrap;
span {
text-align: center;
max-width: 30ch;
overflow: hidden;
text-overflow: ellipsis;
}
}
display: table;
& > * {
display: table-row;
@ -928,9 +963,25 @@
padding: 5px 20px;
vertical-align: middle;
&:first-child {
min-width: 200px;
//overflow: auto;
max-width: 300px;
min-width: 300px;
width: 300px;
position: absolute;
left: 20px;
word-break: break-word;
background: @cp_form-bg1;
z-index: 2;
}
.cp-radio-mark {
&:nth-child(2) {
visibility: hidden;
//overflow: auto;
max-width: 300px;
min-width: 300px;
width: 300px;
word-break: break-word;
}
.cp-radio-mark, .cp-checkmark-mark {
margin: auto;
}
}
@ -942,8 +993,11 @@
.cp-form-type-sort {
cursor: grab;
padding: 5px;
display: flex;
align-items: center;
word-break: break-word;
.cp-form-handle {
margin-right: 5px;
min-width: 12px;
}
.cp-form-sort-order {
border: 1px solid @cryptpad_text_col;
@ -962,22 +1016,24 @@
.cp-form-type-poll-container {
overflow: auto;
.cp-form-poll-hint {
margin-bottom: 10px;
position: absolute;
}
}
.cp-form-type-poll {
margin-top: 32px; // cp-form-poll-hint is "absolute"
display: inline-flex;
flex-flow: column;
width: 100%;
& > div {
display: flex;
}
.cp-poll-total {
display: flex;
width: 100%;
}
.cp-form-poll-body {
flex-flow: column;
max-height: 225px;
overflow: auto;
& > div {
display: flex;
@ -985,15 +1041,15 @@
}
.cp-poll-cell {
width: 100px;
height: 35px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-top:5px;
margin-left:5px;
&:first-child {
width: 200px;
}
flex-shrink: 0;
flex-grow: 1;
word-break: break-word;
button {
width: 100%;
border-top: 0px;
@ -1039,6 +1095,27 @@
.cp-poll-time-day {
flex-basis: 100px;
border-bottom: 1px solid @cryptpad_text_col;
&:first-child {
border-color: @cp_form-bg1 !important;
}
}
.cp-form-poll-option, .cp-poll-time-day {
span {
text-overflow: ellipsis;
max-width: 100%;
overflow: hidden;
line-height: 1.2;
hyphens: auto;
text-align: center;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
.cp-poll-switch {
button.btn {
border-radius: 0px;
}
}
&:not(.cp-form-poll-switch) {
& > div {
@ -1046,13 +1123,25 @@
margin-bottom: 5px;
}
}
.cp-form-poll-body {
&::-webkit-scrollbar {
display: none;
.cp-poll-cell {
&:first-child {
min-width: 200px;
background: #424242;
position: absolute;
margin-left: 0;
background: @cp_form-bg1;
}
&:nth-child(2) {
margin-left: 205px;
}
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.cp-form-poll-option, .cp-poll-time-day, .cp-form-poll-choice {
flex-grow: 1;
flex-shrink: 0;
}
.cp-form-poll-option, .cp-poll-time-day {
flex-flow: column;
text-align: center;
@ -1074,9 +1163,6 @@
flex-flow: column;
&.cp-form-poll-body {
flex-flow: row;
max-width: 550px;
max-height: unset;
scroll-snap-type: x mandatory;
& > div {
flex-flow: column;
}
@ -1091,12 +1177,18 @@
}
}
.cp-form-poll-option, .cp-poll-switch {
span {
max-height: 100%;
}
width: 200px;
.cp-form-weekday-separator {
margin-right: 5px;
margin-left: 5px;
}
}
div.cp-poll-time-day-container {
width: auto !important;
}
.cp-poll-time-day {
flex-basis: 40px;
border-right: none;
@ -1145,6 +1237,34 @@
}
}
#cp-form-settings {
.cp-modal {
text-align: left;
width: 500px;
padding: 24px;
h2 {
font-size: 20px;
line-height: 40px;
margin-top: -12px;
i {
margin-right: 5px;
}
}
& > *:not(h2) {
color: @cryptpad_text_col;
}
& > div:not(:last-child) {
margin-bottom: 10px;
}
.cp-form-setting-title {
color: @cryptpad_color_link;
}
}
}
& > .flatpickr-calendar {
z-index: 100001;
}
.charts_main();
}

View File

@ -13,8 +13,74 @@ define([
value += '"' + vv + '"';
return value;
};
Export.results = function (content, answers, TYPES, order, isArray) {
var exportJSON = function (content, answers, TYPES, order) {
var form = content.form;
var res = {
questions: {},
responses: []
};
var q = res.questions;
var r = res.responses;
// Add questions
var i = 1;
order.forEach(function (key) {
var obj = form[key];
if (!obj) { return; }
var type = obj.type;
if (!TYPES[type]) { return; } // Ignore static types
var id = `q${i++}`;
if (TYPES[type] && TYPES[type].exportCSV) {
var _obj = Util.clone(obj);
_obj.q = "tmp";
q[id] = {
question: obj.q,
items: TYPES[type].exportCSV(false, _obj).map(function (str) {
return str.slice(6); // Remove "tmp | "
})
};
} else {
q[id] = obj.q || Messages.form_default;
}
});
Object.keys(answers || {}).forEach(function (key) {
var userObj = answers[key];
Object.keys(userObj).forEach(function (k) {
var obj = userObj[k];
var time = new Date(obj.time).toISOString();
var msg = obj.msg || {};
var user = msg._userdata || {};
var data = {
'_time': time,
'_name': user.name || Messages.anonymous
};
var i = 1;
order.forEach(function (key) {
if (!form[key]) { return; }
var type = form[key].type;
if (!TYPES[type]) { return; } // Ignore static types
var id = `q${i++}`;
if (TYPES[type].exportCSV) {
data[id] = TYPES[type].exportCSV(msg[key], form[key]);
return;
}
data[id] = msg[key];
});
r.push(data);
});
});
return JSON.stringify(res, 0, 2);
};
Export.results = function (content, answers, TYPES, order, format) {
if (!content || !content.form) { return; }
if (format === "json") { return exportJSON(content, answers, TYPES, order); }
var isArray = format === "array";
var csv = "";
var array = [];
var form = content.form;
@ -39,29 +105,32 @@ define([
array.push(questions);
Object.keys(answers || {}).forEach(function (key) {
var obj = answers[key];
csv += '\n';
var time = new Date(obj.time).toISOString();
var msg = obj.msg || {};
var user = msg._userdata || {};
var line = [];
line.push(time);
line.push(user.name || Messages.anonymous);
order.forEach(function (key) {
var type = form[key].type;
if (!TYPES[type]) { return; } // Ignore static types
if (TYPES[type].exportCSV) {
var res = TYPES[type].exportCSV(msg[key], form[key]);
Array.prototype.push.apply(line, res);
return;
}
line.push(String(msg[key] || ''));
var _obj = answers[key];
Object.keys(_obj).forEach(function (uid) {
var obj = _obj[uid];
csv += '\n';
var time = new Date(obj.time).toISOString();
var msg = obj.msg || {};
var user = msg._userdata || {};
var line = [];
line.push(time);
line.push(user.name || Messages.anonymous);
order.forEach(function (key) {
var type = form[key].type;
if (!TYPES[type]) { return; } // Ignore static types
if (TYPES[type].exportCSV) {
var res = TYPES[type].exportCSV(msg[key], form[key]);
Array.prototype.push.apply(line, res);
return;
}
line.push(String(msg[key] || ''));
});
line.forEach(function (v, i) {
if (i) { csv += ','; }
csv += escapeCSV(v);
});
array.push(line);
});
line.forEach(function (v, i) {
if (i) { csv += ','; }
csv += escapeCSV(v);
});
array.push(line);
});
if (isArray) { return array; }
return csv;

File diff suppressed because it is too large Load Diff

View File

@ -44,6 +44,7 @@ define([
var addRpc = function (sframeChan, Cryptpad, Utils) {
sframeChan.on('EV_FORM_PIN', function (data) {
channels.answersChannel = data.channel;
Cryptpad.changeMetadata();
Cryptpad.getPadAttribute('answersChannel', function (err, res) {
// If already stored, don't pin it again
if (res && res === data.channel) { return; }
@ -122,6 +123,8 @@ define([
return false;
}
};
var deleteLines = false; // "false" to support old forms
sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) {
var cb = Utils.Util.once(_cb);
var myKeys = {};
@ -138,12 +141,13 @@ define([
CPNetflux = _CPNetflux;
Pinpad = _Pinpad;
}));
var personalDrive = !Cryptpad.initialTeam || Cryptpad.initialTeam === -1;
Cryptpad.getAccessKeys(w(function (_keys) {
if (!Array.isArray(_keys)) { return; }
accessKeys = _keys;
_keys.some(function (_k) {
if ((!Cryptpad.initialTeam && !_k.id) || Cryptpad.initialTeam === _k.id) {
if ((personalDrive && !_k.id) || Cryptpad.initialTeam === Number(_k.id)) {
myKeys = _k;
return true;
}
@ -160,6 +164,9 @@ define([
Cryptpad.makeNetwork(w(function (err, nw) {
network = nw;
}));
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
if (md && md.deleteLines) { deleteLines = true; }
}));
}).nThen(function () {
if (!network) { return void cb({error: "E_CONNECT"}); }
@ -183,6 +190,9 @@ define([
validateKey: keys.secondaryValidateKey,
owners: [myKeys.edPublic],
crypto: crypto,
metadata: {
deleteLines: true
}
//Cache: Utils.Cache // TODO enable cache for form responses when the cache stops evicting old answers
};
var results = {};
@ -198,7 +208,6 @@ define([
nThen(function (waitFor) {
accessKeys.forEach(function (obj) {
Pinpad.create(network, obj, waitFor(function (e) {
console.log('done', obj);
if (e) { console.error(e); }
}));
});
@ -222,14 +231,26 @@ define([
config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) {
var parsed = Utils.Util.tryParse(msg);
if (!parsed) { return; }
var uid = parsed._uid || '000';
// If we have a "non-anonymous" answer, it may be the edition of a
// previous anonymous answer. Check if a previous anonymous answer exists
// with the same uid and delete it.
if (parsed._proof) {
var check = checkAnonProof(parsed._proof, data.channel, curvePrivate);
if (check) {
delete results[parsed._proof.key];
var theirAnonKey = parsed._proof.key;
if (check && results[theirAnonKey] && results[theirAnonKey][uid]) {
delete results[theirAnonKey][uid];
}
}
if (data.cantEdit && results[senderCurve]) { return; }
results[senderCurve] = {
parsed._time = cfg && cfg.time;
if (deleteLines) { parsed._hash = hash; }
if (data.cantEdit && results[senderCurve]
&& results[senderCurve][uid]) { return; }
results[senderCurve] = results[senderCurve] || {};
results[senderCurve][uid] = {
msg: parsed,
hash: hash,
time: cfg && cfg.time
@ -239,7 +260,7 @@ define([
});
});
sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) {
var answer;
var answers = [];
var myKeys;
nThen(function (w) {
Cryptpad.getFormKeys(w(function (keys) {
@ -259,38 +280,68 @@ define([
w.abort();
return void cb(obj);
}
answer = obj;
// Get the latest edit per uid
var temp = {};
obj.forEach(function (ans) {
var uid = ans.uid || '000';
temp[uid] = ans;
});
answers = Object.values(temp);
}));
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
if (md && md.deleteLines) { deleteLines = true; }
}));
}).nThen(function () {
if (answer.anonymous) {
if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); }
myKeys = getAnonymousKeys(myKeys.formSeed, data.channel);
}
Cryptpad.getHistoryRange({
channel: data.channel,
lastKnownHash: answer.hash,
toHash: answer.hash,
}, function (obj) {
if (obj && obj.error) { return void cb(obj); }
var messages = obj.messages;
if (!messages.length) { return void cb(); }
if (obj.lastKnownHash !== answer.hash) { return void cb(); }
try {
var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, {
validateKey: data.validateKey,
ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate),
my_private: Nacl.util.decodeBase64(myKeys.curvePrivate),
their_public: Nacl.util.decodeBase64(data.publicKey)
});
var parsed = JSON.parse(res.content);
parsed._isAnon = answer.anonymous;
parsed._time = messages[0].time;
cb(parsed);
} catch (e) {
cb({error: e});
}
var n = nThen;
var err;
var all = {};
answers.forEach(function (answer) {
n = n(function(waitFor) {
var finalKeys = myKeys;
if (answer.anonymous) {
if (!myKeys.formSeed) {
err = 'ANONYMOUS_ERROR';
console.error('ANONYMOUS_ERROR', answer);
return;
}
finalKeys = getAnonymousKeys(myKeys.formSeed, data.channel);
}
Cryptpad.getHistoryRange({
channel: data.channel,
lastKnownHash: answer.hash,
toHash: answer.hash,
}, waitFor(function (obj) {
if (obj && obj.error) { err = obj.error; return; }
var messages = obj.messages;
if (!messages.length) {
// XXX TODO delete from drive.forms
return;
}
if (obj.lastKnownHash !== answer.hash) { return; }
try {
var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, {
validateKey: data.validateKey,
ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate),
my_private: Nacl.util.decodeBase64(finalKeys.curvePrivate),
their_public: Nacl.util.decodeBase64(data.publicKey)
});
var parsed = JSON.parse(res.content);
parsed._isAnon = answer.anonymous;
parsed._time = messages[0].time;
if (deleteLines) { parsed._hash = answer.hash; }
var uid = parsed._uid || '000';
if (all[uid] && !all[uid]._isAnon) { parsed._isAnon = false; }
all[uid] = parsed;
} catch (e) {
err = e;
}
}));
}).nThen;
});
n(function () {
if (err) { return void cb({error: err}); }
cb(all);
});
});
});
@ -304,11 +355,6 @@ define([
// We can create a seed in localStorage.
if (!keys.formSeed) {
// No drive mode
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
if(answered.indexOf(data.channel) !== -1) {
// Already answered: abort
return void cb({ error: "EANSWERED" });
}
keys = { formSeed: noDriveSeed };
}
myKeys = keys;
@ -334,6 +380,8 @@ define([
}
var crypto = Utils.Crypto.Mailbox.createEncryptor(myKeys);
var uid = data.results._uid || Utils.Util.uid();
data.results._uid = uid;
var text = JSON.stringify(data.results);
var ciphertext = crypto.encrypt(text, box.publicKey);
@ -343,15 +391,39 @@ define([
ciphertext
], function (err, response) {
Cryptpad.storeFormAnswer({
uid: uid,
channel: box.channel,
hash: hash,
curvePrivate: ephemeral_private,
anonymous: Boolean(data.anonymous)
}, function () {
var res = data.results;
res._isAnon = data.anonymous;
res._time = +new Date();
if (deleteLines) { res._hash = hash; }
cb({
error: err,
response: response,
results: res
});
});
cb({error: err, response: response, hash: hash});
});
});
});
sframeChan.on("Q_FORM_DELETE_ALL_ANSWERS", function (data, cb) {
if (!data || !data.channel) { return void cb({error: 'EINVAL'}); }
Cryptpad.clearOwnedChannel(data, cb);
});
sframeChan.on("Q_FORM_DELETE_ANSWER", function (data, cb) {
if (!deleteLines) {
return void cb({error: 'EFORBIDDEN'});
}
Cryptpad.deleteFormAnswers(data, cb);
});
sframeChan.on("Q_FORM_MUTE", function (data, cb) {
if (!Utils.secret) { return void cb({error: 'EINVAL'}); }
Cryptpad.muteChannel(Utils.secret.channel, data.muted, cb);
});
};
SFCommonO.start({
addData: addData,

1476
www/lib/asciidoctor/asciidoctor.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,14 @@ define([
};
var getEndDate = function () {
setTimeout(function () { $(endPickr.calendarContainer).remove(); });
return endPickr.parseDate(e.value);
var d = endPickr.parseDate(e.value);
if (endPickr.config.dateFormat === "Y-m-d") { // All day event
// Tui-calendar will remove 1s (1000ms) to the date for an unknown reason...
d.setMilliseconds(1000);
}
return d;
};
return {

View File

@ -12,4 +12,5 @@ This file is intended to be used as a log of what third-party source we have ven
* [Fabricjs 4.6.0](https://github.com/fabricjs/fabric.js) and [Fabric-history](https://github.com/lyzerk/fabric-history) for the whiteboard app
* [Requirejs optional module plugin](https://stackoverflow.com/a/27422370)
* [asciidoc.js 2.0.0](https://github.com/asciidoctor/codemirror-asciidoc/releases/tag/2.0.0) with slight changes to match the format of other codemirror modes
* [Asciidoctor.js 2.2.6](https://github.com/asciidoctor/asciidoctor.js/releases/tag/v2.2.6) for AsciiDoc rendering

View File

@ -1704,7 +1704,7 @@ define([
drive: 'fa fa-hdd-o',
cursor: 'fa fa-i-cursor',
code: 'fa fa-file-code-o',
pad: 'fa fa-file-word-o',
pad: 'cptools cptools-richtext',
security: 'fa fa-lock',
subscription: 'fa fa-star-o',
kanban: 'cptools cptools-kanban',

View File

@ -285,6 +285,22 @@
}
}
.cp-teams-invite-uses { // XXX
input {
margin-bottom: 0px !important;
margin-right: 10px;
width: 75px !important;
}
}
.cp-teams-invite-role {
margin-bottom: 15px;
span:first-child {
margin-right: 10px;
}
}
#cp-teams-roster-dialog {
table {
width: 100%;

View File

@ -17,6 +17,7 @@ define([
'/customize/application_config.js',
'/common/messenger-ui.js',
'/common/inner/invitation.js',
'/common/clipboard.js',
'/common/make-backup.js',
'/customize/messages.js',
@ -43,6 +44,7 @@ define([
AppConfig,
MessengerUI,
InviteInner,
Clipboard,
Backup,
Messages)
{
@ -714,6 +716,18 @@ define([
title: Messages.team_pendingOwnerTitle
}, ' ' + Messages.team_pendingOwner));
}
if (data.pending && data.inviteChannel && data.remaining === -1) { // Invite link
$(name).append(h('em', ' ' + Messages.team_linkUsesInfinite));
} else if (data.pending && data.inviteChannel) {
$(name).append(h('em', ' ' + Messages._getKey('team_linkUses', [
data.remaining || 1,
data.totalUses || 1
])));
}
if (data.pending && data.inviteChannel) {
var r = data.role === "MEMBER" ? Messages.team_members : Messages.team_viewers;
$(name).append(h('em', ' (' + r + ')'));
}
// Status
var status = h('span.cp-team-member-status'+(data.online ? '.online' : ''));
// Actions
@ -817,6 +831,23 @@ define([
actions,
status,
];
if (data.inviteChannel) {
if (data.hash) {
var copy = h('span.fa.fa-copy');
$(copy).click(function () {
var privateData = common.getMetadataMgr().getPrivateData();
var origin = privateData.origin;
var href = origin + Hash.hashToHref(data.hash, 'teams');
var success = Clipboard.copy(href);
if (success) { UI.log(Messages.shareSuccess); }
}).prependTo(actions);
}
content = [
avatar,
name,
actions
];
}
var div = h('div.cp-team-roster-member', content);
if (data.profile) {
$(div).dblclick(function (e) {
@ -872,7 +903,7 @@ define([
if (!roster[k].pending) { return; }
if (!roster[k].inviteChannel) { return; }
roster[k].curvePublic = k;
return roster[k].role === "VIEWER" || !roster[k].role;
return roster[k].role === "MEMBER" || roster[k].role === "VIEWER" || !roster[k].role;
}).map(function (k) {
return makeMember(common, roster[k], me);
});
@ -1349,7 +1380,7 @@ define([
Messages._getKey('team_inviteFromMsg',
[Util.fixHTML(getDisplayName(json.author.displayName)),
Util.fixHTML(json.teamName)])));
if (typeof(json.message) === 'string') {
if (typeof(json.message) === 'string' && json.message) {
var message = h('div.cp-teams-invite-message');
json.message.split('\n').forEach(line => {
if (line.trim()) {
@ -1524,12 +1555,12 @@ define([
$div.empty().append(content);
});
}
var $divLink = $('div.cp-team-link').empty();
/*var $divLink = $('div.cp-team-link').empty();
if ($divLink.length) {
refreshLink(common, function (content) {
$divLink.append(content);
});
}
}*/
var $divCreate = $('div.cp-team-create');
if ($divCreate.length) {
refreshCreate(common, function (content) {