Merge main branch

This commit is contained in:
Ludovic Dubost 2020-08-06 14:43:01 +02:00
commit 7ee9705971
251 changed files with 21184 additions and 20823 deletions

View File

@ -1,7 +0,0 @@
data
Dockerfile
docker-compose.yml
.dockerignore
.git
.gitignore
node_modules

4
.env
View File

@ -1,4 +0,0 @@
VERSION=latest
USE_SSL=true
STORAGE='./storage/file'
LOG_TO_STDOUT=true

View File

@ -1,5 +1,6 @@
node_modules/
www/bower_components/
www/common/less.min.js
www/common/pdfjs/
www/common/tippy/
www/common/textFit.min.js
@ -14,6 +15,7 @@ www/common/onlyoffice/v2*
server.js
www/common/old-media-tag.js
www/scratch
www/lib
www/common/toolbar.js
www/common/hyperscript.js
@ -35,3 +37,4 @@ www/debug/chainpad.dist.js
www/pad/mathjax/
www/code/mermaid*.js
www/code/orgmode.js

View File

@ -1,11 +0,0 @@
language: node_js
branches:
only:
- master
- soon
- staging
node_js:
- "6.6.0"
script:
- npm run-script lint
- docker build -t xwiki/cryptpad .

View File

@ -1,3 +1,348 @@
# UplandMoa's revenge (3.20.1)
Once again we've decided to follow up our last major release with a minor "revenge" release that we wanted to make available as soon as possible.
We expect to deploy and release version 3.21.0 on Tuesday, July 28th, 2020.
Features
* The _markmap_ rendering mode which was recently added to markdown preview pane implements some click event handlers which overlap with our existing handlers which open the embedded mindmap in our full screen "lightbox". You can now use _ctrl-click_ to trigger its built-in events (collapsing subtrees of the mindmap) without opening the lightbox.
* We've made a few improvement to user and team drives:
* The _list mode_ now features a "ghost icon" which you can use to create a new pad in the current folder, matching behaviour that already existed in grid mode.
* We've also updated the search mode to display a spinner while your search is in progress. We also display some text when no results are found.
* Team drives now open with the sidebar collapsed.
* Our rich text, code, slide, and poll apps now intercept pasted images and prompt the user to upload them, matching the existing experience of dragging an image into the same editable area.
* We've received new contributions to our Romanian translation via [our weblate instance](https://weblate.cryptpad.fr/projects/cryptpad/app/).
Bug fixes
* We identified some race conditions in our spreadsheet app that were responsible for some corrupted data during the period leading up to our 3.20.0 release, however, we wanted to take a little more time to test before releasing the fixes. As of this release we're moving to a third version of our internal data format. This requires a client-side migration for each older sheet which will be performed by the first registered user to open a sheet in edit mode, after which a page reload will be required. Unregistered users with edit rights will only be able to view older sheets until they have been migrated by a registered user.
* We now guard against empty _mathjax_ and _markmap_ code blocks in their respective markdown preview rendering extensions, as we discovered that empty inputs resulted in the display of "undefined" in the rendered element.
* We noticed and fixed two regressions in user and team drives:
1. drive history had stopped working since the introduction of the "restricted mode" for shared folders which were made inaccessible due to the enforcement of their access lists.
2. users with shared folders which had been deleted or had their passwords changed were prompted to delete the folder from their drive or enter its new password. The "submit" button was affected by a style regression which we've addressed.
* We've updated to a new version of `lodash` as a dependency of the linters that we use to validate our code. Unless you were actively using those linters while developing CryptPad this should have no effect for you.
* Finally, when users open a link to a "self-destructing pad" we now check to make sure that the deletion key they possess has not been revoked before displaying a warning indicating that the pad in question will be deleted once they open it.
To update from 3.20.0 to 3.20.1:
1. Stop your server
2. Get the latest code with `git checkout 3.20.1`
3. Install the latest dependencies with `bower update` and `npm i`
3. Restart your server
# UplandMoa (3.20.0)
## Goals
We've held off on deploying any major features while we work towards deploying some documentation we've been busy organizing. This release features a wide range of minor features intended to address a number of github issues and frequent causes of support tickets.
## Update notes
This release features a modification to the recommended Content Security Policy headers as demonstrated in `./cryptpad/docs/example.nginx.conf`. CryptPad will work without making this change, however, we highly recommend updating your instance's nginx.conf as it will mitigate a variety of potential security vulnerabilities.
Otherwise, we've introduced a new client-side dependency (_Mathjax_) and changed some server-side code that will require a server restart.
To update from 3.19.1 to 3.20.0:
1. Apply the recommended changes to your `nginx.conf`
2. Stop your server
3. Get the latest platform code with git
4. Install client-side dependencies with `bower update`
5. Reload nginx to apply the updated CSP headers
6. Restart the CryptPad API server
## Features
* As noted above, this release features a change to the Content Security Policy headers which define the types of code that can be loaded in a given context. More specifically, we've addressed a number of CKEditor's quirks which required us to set a more lax security policy for the rich text editor. With these changes in place the only remaining exceptions to our general policy are applied for the sake of our OnlyOffice integration, though we hope to address its quirks soon as well.
* On the topic of the rich text editor, we also moved the _print_ action from the CKEditor toolbar to the _File_ menu to be more consistent with our other apps.
* The Kanban board that we use to organize our own team has become rather large and complex due to a wealth of long-term ideas and a large number of tags. We started to notice some performance issues as a result, and have begun looking into some optimizations to improve its scalability. As a start, we avoid applying changes whenever the Kanban's tab is not visible.
* We finally decided to file off one of the platform's rough edges which had been confusing curious users for some time. Every registered user is identified by a randomly-generated cryptographic key (the _Public Signing Key_ found on your settings page). These identifiers are used to allocate additional storage space via our premium accounts, and we occasionally require them for other support issues like deleting accounts or debugging server issues. Unfortunately, because we occasionally receive emails asking for help with _other administrators instances_ these keys were formatted along with the host domain in the form of a URL. As such, it was very tempting to open them in the browser even though there was no functionality corresponding to the URL. We've updated all the code that parses these keys and introduced a new format which is clearly _not a URL_, so hopefully we'll get fewer messages asking us why they _don't work_.
* We've made a number of small improvements to the common functionality in our code and slide editors:
* We've merged and built upon a pull request which implemented two new extensions to our markdown renderer for _Mathjax_ and _Markmap_. This introduces support for embedding formatted equations and markdown-based mind maps. Since these depend on new client-side code which would otherwise increase page loading time we've also implemented support for lazily loading extensions on demand, so you'll only load the extra code if the current document requires it.
* The _slide_ editor now throttles slide redraws so that updates are only applied after 400ms of inactivity rather than on every character update.
* We've made a number of small style tweaks for blockquotes, tables, and embedded media in rendered markdown.
* Lastly, we've made a large number of improvements to user and team drives:
* Search results now include shared folders with matching names and have been made _sortable_ like the rest of the drive.
* Inserting media in a document via the _Insert_ menu now updates its access time, which causes it to show up in the _Recent pads_ category of your drive.
* Shared folders now support access lists. To apply an access list to a shared folder that you own you may right-click the shared folder in your drive, choose _Access_, then click the _List_ tab of the resulting dialog. Enabling its access list will restrict access to its owners and any other contacts that you or other owners add to its list. Note, this access applies to the folder itself (who can view it or add to its directory), its access list will not be applied recursively to all the elements contained within which might be contained in other shared folders or other users drives.
* In the interest of removing jargon from the platform we've started to change text from "Delete from the server" to "Destroy". We plan to make more changes like this on an ongoing basis as we notice them.
* We've made a significant change to the way that _owned files_ are treated in the user and team drives. Previously, files that you owned were implicitly deleted from the server whenever you removed them from your drive. This seemed sensible when we first introduced the concept of ownership, however, now that a variety of assets can have multiple owners it is clearly less appropriate. Rather than require users to first remove themselves as a co-owner before removing an asset from their drive in order to allow other owners to continue accessing it we now offer two distinct _Remove_ and _Destroy_ actions. _Remove_ will simply take it out of your drive so that it will no longer count against your storage limit, while _Destroy_ will cause it to stop existing _for everyone_. To clarify the two actions we've associated them with a _trash bin_ and _paper shredder_ icon, respectively.
## Bug fixes
* Remote changes in the Kanban app removed pending text in new cards, effectively making it impossible (and very frustrating) to create new cards while anyone else was editing existing content or submitting their own new cards.
* Dropping an image directly into a spreadsheet no longer puts the UI into an unrecoverable state, though we still don't support image drop. To insert images, use the "Insert" menu. This was actually fixed in our 3.19.1 release, but it wasn't documented in the release notes.
* When a user attempted to open an automatically expiring document which had passed its expiration date they were shown a general message indicating that the document had been deleted even when they had sufficient information to know that it had been marked for expiration. We now display a message indicating the more likely cause of its deletion.
* We've spent some time working on the usability of comments in our rich text app:
* When a user started adding a first comment to a document then canceled their action it was possible for the document to get stuck in an odd layout. This extra space allocated towards comments now correctly collapses as intended when there are no comments, pending or otherwise.
* The comments UI is now completely disabled whenever the document is in read-only mode, whether due to disconnection or insufficient permissions.
* The _comment_ button in the app toolbar now toggles on and off to indicate the eligibility of the current selection as a new comment.
* We've fixed a number of issues with teams:
* Users no longer send themselves a notification when they remove themself as an owner of a pad from within the _Teams_ UI.
* The _worker_ process which is responsible for managing account rights now correctly upgrades and downgrades its internal state when its role within a team is changed by a remote user instead of requiring a complete worker reload.
* The worker does not delete credentials to access a team when it finds that its id is not in the team's roster, since this could be triggered accidentally by some unrelated server bugs that responded incorrectly to a request for the team roster's history.
* We've fixed a number of issues in our code and slide editors:
* The "Language" dropdown selectors in the "Theme" menu used to show "Language (Markdown)" when the page was first loaded, however, changing the setting to another language would drop the annotation and instead show only "Markdown". Now the annotation is preserved as intended.
* A recent update to our stylesheets introduced a regression in the buttons of our "print options" dialog.
* While polishing up the PRs which introduced the _Mathjax_ and _Markmap_ support we noticed that the client-side cache which is used to prevent unnecessary redraws of embedded media was causing only one instance of an element to be rendered when the same source was embedded in multiple sections of a document.
* The "File export" dialog featured a similar regression in the style of its buttons which has been addressed.
* We fixed a minor bug in our 3.19.0 release in which unregistered users (who do not have a "mailbox") tried to send a notification to themselves.
* We've added an additional check to the process for changing your account password in which we make sure that we are not overwriting another account with the same username and password.
# Thylacine's revenge (3.19.1)
Our upcoming 3.20.0 release is planned for July 7th, 2020, but we are once again releasing a minor version featuring some nice bug fixes and usability improvements which are ready to be deployed now. In case you missed [our announcement](https://social.weho.st/@cryptpad/104360490068671089) we are phasing out our usage of the `master` and basing our releases on the `main` branch. For best results we recommend explicitly checking out code by its tag.
New features:
* We've spent a little time making support tickets a little bit easier for both users and admins.
* Users can now label their tickets with a set of predefined categories, making it easier for admins to sort through related reports.
* Users and admins can both attach encrypted uploads to their messages, making it easier to demonstrate a problem with an image, video, or other example file.
* Teams now take advantage of the same "mailbox" functionality that powers user accounts' notification center. Team members with the "viewer" role can now use this feature to share documents with their team using the "share menu" as they already can with other users. Anyone with the ability to add a document to the team's drive will then be able to receive the notification and add the document to the team's drive for them. Going forward we'll use this functionality to implement more behaviour to make teams function more like shared user accounts.
* The "pad creation screen" which is displayed to registered users when they first create a pad will no longer remember the settings used when they last created a pad. While this behaviour was intended to streamline the process of creating documents, in practice it led to some user's documents getting deleted because they didn't realize they were set to automatically expire. If you prefer not to use the defaults (owned, non-expiring) then you'll have to click a few more times to create a document, but we think that's a worthwhile tradeoff to avoid data loss.
Bug fixes:
* Hitting _ctrl-A_ in the drive used to select lots of the page's elements which had no business being selected. Now it will select the contents of the directory currently being displayed.
* Due to some complications in OnlyOffice (which we use for spreadsheets) remote updates made to a sheet were not displayed for users who had opened the document in "view mode". We still don't have the means to apply these remote changes in real-time, but we now prompt users to click a button to refresh the editor (not the full page) to display the latest document state.
* A recent update set the text color of the team chat input to 'white', matching the input's background and making the text unreadable. We patched it to make it black text on a white background.
* We're slowly working on improving keyboard shortcuts for a variety of actions. This time around we fixed a bug that prevented "ESC" from closing an open "tag prompt" interface.
* We noticed that the zip file constructed in the browser when you downloaded a subtree of a shared folder in your drive contained the correct directory structure but did not contain the files that were supposed to be there. This has been fixed.
* Finally, we've tweaked our styles to use more specific CSS selectors to prevent a variety of styles from being accidentally applied to the wrong elements. This should make the platform a little easier to maintain and help us improve the visual consistency of a variety of elements on different pages.
To update from 3.19.0 to 3.19.1:
1. Stop your server
2. Get the latest code with `git checkout 3.19.1`
3. Restart your server
If you're updating from anything other than 3.19.0 you may need other clientside dependencies (available with `bower update` and `npm i`).
# Thylacine release (3.19.0)
## Goals
The intent of this release was to catch up on our backlog of bug fixes and minor usability improvements.
## Update notes
This release features an update to our clientside dependencies.
To update to 3.19.0 from 3.18.1:
1. Stop your server
2. Get the latest code with git
3. Get the latest clientside dependencies with `bower update`
4. Restart your server
## Features
* The most notable change in this release is that the use of "safe links" (introduced in our 3.11.0 release) has been made the new default for documents. This means that when you open a document that is stored in your drive your browser's address bar will not contain the encryption keys for the document, only an identifier used to look up those encryption keys which are stored in your drive. This makes it less likely that you'll leak access to your documents during video meetings, when sharing screenshots, or when using shared computers that store the history of pages you've viewed.
* To share access to documents with links, you'll need to use the _share menu_ which has recently been made more prominent in the platform's toolbars
* This setting is configurable, so you can still choose to disable the use of safe links via your settings page.
* We've updated the layout of the "user admin menu" which can be found in the top-right corner by clicking your avatar. It features an "About CryptPad" menu which displays the version of the instance you're using as well as some resources which are otherwise only available via the footer of static pages.
* We often receive support tickets in languages that we don't speak, which forces us to use translation services in order to answer questions. To address this issue, we've made it possible for admins to display a notice indicating which languages they speak. An example configuration is provided in `customize.dist/application_config.js`.
* We've integrated two PRs:
1. [Only list premium features when subscriptions are enabled](https://github.com/xwiki-labs/cryptpad/pull/538).
2. [Add privacy policy option](https://github.com/xwiki-labs/cryptpad/pull/537).
* We found it cumbersome to add new cards to the top of our Kanban columns, since we had to create a new card at the bottom and then drag it to the top. In response, we've broken up the rather large "new card" button into two buttons, one which adds a card at the top, and another which adds a new card at the bottom.
* We've made it easier to use tags for files in the drive:
1. You can now select multiple files and apply a set of tags to all of them.
2. Hitting "enter" in an empty tag prompt field will submit the current list of tags.
* We've also made a few tweaks to the kanban layout:
1. The "trash bar" only appears while you are actively dragging a card.
2. The "tag list" now takes up more of the available width, while the button to clear the currently applied tag filter has been moved to the left, replacing the "filter by tag" hint text.
* We've received requests to enable translations for a number of languages over the last few months. The following languages are enabled on [our weblate instance](https://weblate.cryptpad.fr/projects/cryptpad/app/), but have yet to be translated.
* Arabic
* Hindi
* Telugu
* Turkish
* Unregistered users were able to open up the "filepicker modal" in spreadsheets. It was already possible to embed an image which they'd already stored in their drive, but it was not clear why they were not able to upload a new image. We now display a disabled upload button with a tooltip to log in or register in order to upload images.
* Finally, we've updated the styles in our presentation editor to better match our recent toolbar redesign and the mermaidjs integration.
## Bug fixes
* We now preserve formatting in multi-line messages in team invitations.
* The slide editor exhibited some strange behaviour where the page would reload the first time you entered "present mode" after creating the document. We've also fixed some issues with printing.
* We now prevent the local resizing of images in the rich text editor while it is locked due to disconnection or the lack of edit rights.
* We've updated our marked.js dependency to the latest version in order to correct some minor rendering bugs.
* Unregistered users are now redirected to the login page when they visit the support page.
* We've removed the unsupported "rename" entry from the right-click menu in unregistered users drives.
* After a deep investigation we found and fixed the cause of a bug in which user accounts spontaneously removed themselves from teams. A flaw in the serverside cache caused clients to load an incomplete account of the team's membership which caused the team to appear to have been deleted. Unfortunately, the client responded by removing the corrupt team credentials from their account. Our fix will prevent future corruptions, but does not restore unintentionally removed teams.
* Lastly, we've added a "Hind" font to the spreadsheet editor which introduces basic support for Devanagari characters.
# Smilodon's revenge (3.18.1)
Our next major release (3.19.0) is still a few weeks away.
In the meantime we've been working on some minor improvements and bug fixes that we wanted to ship as soon as possible.
New features:
* Rich text pads can now be exported to .doc format. A few features don't translate well to the exported format (some fonts, embedded videos and pdfs), but for the most part your documents should work
* Items in the "Recent pads" section of your drive can now be dragged to other folders via the filesystem tree UI
* The user admin menu (found in the top-right corner) now includes an option to display the current version of the CryptPad instance you're using. We plan to add some more information here in the near future.
* The kanban app now offers better support for editing markdown within cards with autocompleted parentheses. We've also added support for embedded media, allowing users to drag images and other content into the card content editor.
Bug fixes:
* Account deletion via the settings page works once again
* Some small layout and usability issues in the drive have been addressed
* dropdown menus flow in the appropriate direction when space is limited
* changing the sorting criteria no longer causes the browser to jump to the top of the page
* Hitting enter or escape in the kanban's card tag field while it's empty now closes the modal (instead of doing nothing)
* Language preferences (as configured via the settings page) are applied when you log in (previously it would reset to English or your browser's settings)
* A performance issue triggered by hiding a closed support ticket from the admin panel has been optimized. Previously it would lock up the shared worker in cases when there were many unclosed tickets.
* We've updated the parameters of the XLSX import/export functionality to prevent an "out of memory" error that primarily affected large spreadsheets. It should now allocate more memory instead of failing silently.
* Finally, members of a team can now directly share or transfer ownership of a document owned by their team to their own account without having to go through the additional steps of offering it to themself and accepting the offer.
Updating from 3.18.0 to 3.18.1 is pretty standard:
1. Stop your server
2. Get the latest code with git
3. Restart your server
# Smilodon release (3.18.0)
## Goals
This is a big one! A lot of people are going to love it and a few are probably going to hate it.
This release introduces some major changes to our apps' appearances with the intent of making it easier to use, easier for us to support, and easier to maintain.
## Update notes
If you're using a mostly standard CryptPad installation this should be a rather easy update.
If you've customized your styles, particularly for the purpose of overriding the default colors, you may encounter some problems. **We recommend that you test this version in a staging environment** before deploying to ensure that it is compatible with your modifications.
Otherwise, update to 3.18.0 from 3.17.0 in the following manner:
1. stop your server
2. fetch the latest code with git
3. bower update
4. relaunch your server
## Features
* Obviously, there's the major redesign mentioned in our _goals_.
* You'll immediately notice that we've changed a lot of our color scheme. Apps still have colors as accents to help differentiate them, but the colors are more subtle. The move towards a more monochrome design makes it easier for us to ensure that the UI has a sufficient amount of contrast (less eye strain for everybody!) and simplifies design issues by settling on a simpler color palette.
* You'll probably also notice that a lot of the toolbar features have been rearranged. The chat and userlist are now at the right, while we've adopted the "File menu" layout to which users of office productivity are accustomed. A lot of the common features that were buried in our `...` menu are now under "File" ("new", "import/export", "history", "move to trash", etc.). Some apps feature their special menus ("Insert", "Tools", "Theme") depending on whether they support certain features. In general we'll use text in addition to icons in the toolbar except on very small screens where the use of space is constrained.
* Finally, you'll find some of CryptPad's most important functionality right in the center of the toolbar. The "Share" and "Access" buttons already existed, but lots of people had trouble finding them and missed out on our fine-grained access controls by always sharing the URL directly from their browser's address bar. In case you hadn't seen it, the "Share menu" gives you the ability to generate links that let others view, edit, or delete the document in question. The "Access menu" provides an overview of the document's access settings, and lets its owner(s) add passwords, enable or disable other viewers' ability to request edit rights, restrict access to a dynamic list of users or teams, and modify ownership of the document. It will soon be even more important to know about these menus, because **we plan to enable "Safe links" as the default behaviour in our next release**. "Safe links" are URLs that contain only a document's id instead of its cryptographic secrets, making it less likely that you'll accidentally leak the ability to read your documents during screenshots or when copy-pasting URLs.
* The toolbar redesign has also affected the drive interface, but it's special enough that it deserves a separate mention:
* You can now collapse the sidebar which contains the search button, recent pads, filesystem tree, templates, trash, and account storage quota meter. This should make navigation of the drive on mobile devices much simpler.
* The actual "search" interface is no longer inside the sidebar. Instead, clicking search will bring you to an interface which uses the full size available to display the search bar and its results.
* By the time the toolbar was mostly redesigned we realized that our mockups hadn't included a link to the "todo" app. In fact, we'd been meaning to deprecate it in favour of Kanbans for some time, but we hadn't gotten around to it. So, now there's a migration that will be run automatically when you access your account for the first time after this release. Your todo-list will be transformed into a Kanban located in the root of your drive.
* On that note, this release also makes it much easier to drag and drop kanban cards within and between full columns thanks to an improved scrolling behaviour while you are holding a card.
## Bug fixes
* While implementing the todo-list migration we noticed that user accounts were running migrations without updating their version afterward. This resulted in redundant migrations being run at login time, so now that the version has been updated you might notice that login is marginally faster.
* We also fixed a regression in the "Print" functionality of the rich text editor, so you should be able to print correctly-formatted rich text documents once more.
* Lastly, there were some rather annoying issues with spreadsheets throughout this release that resulted in some users not being able to load their sheets or in their sheets being rendered or encoded incorrectly. We spent a lot of time solving these issues, and believe spreadsheets to be stable once more.
# RedGazelle's revenge release (3.17.1)
In recent months a growing amount of our time has been going towards answering support tickets, emails, and GitHub issues. This has made it a little more difficult to also maintain a bi-weekly release schedule, since there's some overhead involved in deploying our latest code and producing release notes.
To ease our workload, we've decided to switch to producing a full release every three weeks, with an optional patch release at some point in the middle. Patch releases may fix major issues that can't wait three weeks or may simply consist of a few minor fixes that are trivial to deploy.
This release fixes a few spreadsheet issues and introduces a more responsive layout for user drives in list mode.
Updating to 3.17.1 from 3.17.0 is pretty standard:
1. Stop your server
2. Get the latest code with git
3. Restart your server
# RedGazelle release (3.17.0)
## Goals
Our goal for this release was to introduce a first version of comments and mentions in our rich text editor as a part of a second R&D project funded by [NLnet](https://nlnet.nl/). We also received the results of an "accessibility audit" that was conducted as a part of our first NLnet PET project and so we've begun to integrate the auditor's feedback into the platform.
Otherwise we've continued with our major goal of continuing to support a growing number of users on our instance via server improvements (without introducing any regressions).
## Update notes
The most drastic change in this release is that we've removed all docker-related files from the platform's repository. These files were all added via community contributions. Having them in the main repo gave the impression that we support installation via docker (which we do not).
Docker-related files can now be found in the community-support [cryptpad-docker](https://github.com/xwiki-labs/cryptpad-docker/) repository.
If you have an existing instance that you've installed using docker and you'd like to update, you may review the [migration guide](https://github.com/xwiki-labs/cryptpad-docker/blob/master/MIGRATION.md). If you encounter any problems in the process we advise that you create an issue in the repository's issue-tracker.
Once again, this repository is **community-maintained**. If you are using this repository then _you are a part of the community_! Bug reports are useful, but fixes are even better!
Otherwise, this is a fairly standard release. We've updated two of our client-side dependencies:
1. ChainPad features a memory management optimization which is particularly relevant to editing very large documents or loading a drive with a large number of files. In one test we were able to reduce memory consumption in Chrome from 1.7GB to 20MB.
2. CKEditor (the third-party library we use for our rich-text editor) has been updated so that we could make use of some more recent APIs for the _comments_ feature.
To update from **3.16.0** to **3.17.0**:
1. Stop your server
2. Fetch the latest source with git
3. Install the latest client-side dependencies with `bower update`
4. Restart your server
## Features
* As noted above, this release introduces a first version of [comments at the right of the screen](https://github.com/xwiki-labs/cryptpad/issues/143) in our rich text editor. We're aware of a few usability issues under heavy concurrent usage, and we have some more improvements planned, but we figured that these issues were minor enough that people would be happy to use them in the meantime. The comments system integrates with the rest of our social functionality, so you'll have the ability to mention other users with the `@` symbol when typing within a comment.
* We've made some minor changes to the server's logging system to suppress some uninformative log statements and to include some useful information in logs to improve our ability to debug some serverside performance issues. This probably won't affect you directly, but indirectly you'll benefit from some bug fixes and performance tweaks as we get a better understanding of what the server does at runtime.
* We've received an _enormous_ amount of support tickets on CryptPad.fr (enough that if we answered them all we'd have very little time left for development). In response, we've updated the support ticket inbox available to administrators to highlight unanswered messages from non-paying users in yellow while support tickets from _premium users_ are highlighted in red. Administrators on other instances will notice that users of their instance with quotas increased via the server's `customLimits` config block will be counted as _premium_ as well.
* Finally, we've continued to receive translations in a number of languages via our [Weblate instance](https://weblate.cryptpad.fr/projects/cryptpad/app/).
## Bug fixes
* We've fixed a minor bug in our code editor in which hiding _author colors_ while they were still enabled for the document caused a tooltip containing `undefined` to be displayed when hovering over the text.
* A race condition in our server which was introduced when we started validating cryptographic signatures in child processes made it such that incoming messages could be written to the database in a different order than they were received. We implemented a per-channel queue which should now guarantee their ordering.
* It used to be that an error in the process of creating a thumbnail for an encrypted file upload would prevent the file upload from completing (and prevent future uploads in that session). We've added some guards to catch these errors and handle them appropriately, closing [#540](https://github.com/xwiki-labs/cryptpad/issues/540).
* CryptPad builds some CSS on the client because the source files (written in LESS) are smaller than the produced CSS. This results in faster load times for users with slow network connections. We identified and fixed bug in the loader which caused some files to be included in the compiled output multiple times, resulting in faster load times.
* We addressed a minor bug in the drive's item sorting logic which was triggered when displaying inverse sortings.
* Our last release introduced a set of custom styles for the mermaidjs integration in our code editor and featured one style which was not applied consistently across the wide variety of elements that could appear in mermaid graphs. As such, we've reverted the style (a color change in mermaid `graph` charts).
* In the process of implementing comments in our rich text editor we realized that there were some bugs in our cursor recovery code (used to maintain your cursor position when multiple people are typing in the same document). We made some small patches to address a few very specific edge cases, but it's possible the improvements will have a broader effect with cursors in other situations.
* We caught (and fixed) a few regressions in the _access_ and _properties_ modals that were introduced in the previous release.
* It came to our attention that the script `cryptpad/scripts/evict-inactive.js` was removing inactive blobs after a shorter amount of time than intended. After investigating we found that it was using `retentionTime` instead of `inactiveTime` (both of which are from the server's config file. As such, some files were being archived after 15 days of inactivity instead of 90 (in cases where the files were not stored in anyone's drive). This script must be run manually (or periodically via a `cron`), so unless you've configured your instance to do so this will not have affected you.
# Quagga release (3.16.0)
## Goals
We've continued to keep a close eye on server performance since our last release while making minimal changes. Our goal for this release has been to improve server scalability further while also addressing user needs with updates to our client code.
We were pleasantly surprised to receive a pull request implementing a basic version of [author colors](https://github.com/xwiki-labs/cryptpad/issues/41) in our code editor. Since it was nearly ready to go we set some time aside to polish it up a little bit to include it in this release.
## Update notes
We've updated the example nginx config in order to include an `Access-Control-Allow-Origin` header that was not included. We've also added a new configuration point in response to [this issue](https://github.com/xwiki-labs/cryptpad/issues/529) about the server's child processes using too many threads. Administrators may not set a maximum number of child processes via `config.js` using `maxWorkers: <number of child processes>`. We recommend using one less than the number of available cores, though one worker should be sufficient as long as your server is not under heavy load.
As usual, updating from the previous release can be accomplished by:
1. stopping your server
2. pulling the latest code with git
3. installing clientside dependencies with `bower update`
4. installing serverside dependencies with `npm i`
5. restarting your server
## Features
* As mentioned above, we've built upon a very helpful [PR](https://github.com/xwiki-labs/cryptpad/pull/522) from members of the Piratenpartei (German Pirate Party) to introduce author colors in our code editor. It's still experimental, but registered users can enable it on pads that they own via the "Author colors" entry in the `...` menu found beneath their user admin menu.
* Serverside performance optimizations
* Automatically expiring pads work by creating a task to be run at the target date. This process involves a little bit of hashing, so we've changed it to be run in the worker.
* The act of deleting a file from the server actually moves it to an archive which is not publicly accessible. These archived files are regularly cleaned up if you run `scripts/evict-inactive.js`. Unfortunately, moving files is more expensive than deletion, so we've noticed spikes in CPU when users delete many files at once (like when emptying the trash from their drive). To avoid such spikes while the server is already under load we've implemented per-user queues for deletion.
* We've also noticed that when we restart our server while it is under heavy load some queries can time out due to many users requesting history at once. We've implemented another queue to delegate tasks to workers in the order that they are received. We need to observe how this system performs in practice, so there might be small tweaks as we get more data.
* As noted above, we've made the number of workers configurable. At the same time we unified two types of workers into one, cutting the number of workers in half.
* We've added a new admin RPC call to request some information about the server's memory usage to help us debug what seems to be a small memory leak.
* Most of our editors were previously loaded with two more iframes on the page in addition to our main sandboxed iframe. These separate frames ensure that encryption keys are not exposed to the same iframe responsible for displaying the rest of CryptPad's UI. One was responsible for loading the "filepicker" for inserting media into your documents, the other was responsible for handling encryption keys for the share modal. Since we wanted to add two new functions using iframes in the same manner we took the opportunity to come up with a generic solution using only one iframe for these separate modals, since they all have the same level of privilege to the sensitive data we're trying to protect.
* Our mermaidjs integration has been customized to be a little easier on the eyes. We focused in particular on GANTT charts, though other charts should be more appealing as well, especially in the new "lightbox" UI introduced in our last release.
* We now prompt unregistered users to register or log in when they use the spreadsheet editor. For context, unregistered users don't benefit from all of the same features as registered users, and this makes a few performance optimizations impossible.
* Finally, we've continued to receive translations from contributors in Catalan, German, and Dutch.
## Bug fixes
* We noticed that under certain conditions clients were sending metadata queries to the server for documents that don't have metadata. We've implemented some stricter checks to prevent these useless queries.
* We've implemented a temporary fix for our rich text editor to solve [this issue](https://github.com/xwiki-labs/cryptpad/issues/526) related to conflicting font-size and header styles.
* We also accepted [this PR](https://github.com/xwiki-labs/cryptpad/pull/525) to tolerate server configurations specifying a `defaultStorageLimit` of 0.
* Finally, we noticed that embedded media occasionally stopped responding correctly to right-click events due to a problem with our in-memory cache. It has since been fixed.
# PigFootedBandicoot release (3.15.0)
## Goals

View File

@ -1,45 +0,0 @@
# We use multi stage builds
FROM node:12-stretch-slim AS build
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -yq git jq python curl
RUN npm install -g bower
# install tini in this stage to avoid the need of jq and python
# in the final image
ADD docker-install-tini.sh /usr/local/bin/docker-install-tini.sh
RUN /usr/local/bin/docker-install-tini.sh
COPY . /cryptpad
WORKDIR /cryptpad
RUN npm install --production \
&& npm install -g bower \
&& bower install --allow-root
FROM node:12-stretch-slim
# You want USE_SSL=true if not putting cryptpad behind a proxy
ENV USE_SSL=false
ENV STORAGE="'./storage/file'"
ENV LOG_TO_STDOUT=true
# Persistent storage needs
VOLUME /cryptpad/cfg
VOLUME /cryptpad/datastore
VOLUME /cryptpad/customize
VOLUME /cryptpad/blobstage
VOLUME /cryptpad/block
VOLUME /cryptpad/blob
VOLUME /cryptpad/data
# Copy cryptpad and tini from the build container
COPY --from=build /sbin/tini /sbin/tini
COPY --from=build /cryptpad /cryptpad
WORKDIR /cryptpad
# Unsafe / Safe ports
EXPOSE 3000 3001
# Run cryptpad on startup
CMD ["/sbin/tini", "--", "/cryptpad/container-start.sh"]

View File

@ -21,10 +21,10 @@
"jquery": "2.2.4",
"tweetnacl": "0.12.2",
"components-font-awesome": "^4.6.3",
"ckeditor": "4.7.3",
"ckeditor": "4.14.0",
"codemirror": "^5.19.0",
"requirejs": "2.3.5",
"marked": "0.5.0",
"marked": "1.1.0",
"rangy": "rangy-release#~1.3.0",
"json.sortify": "~2.1.0",
"secure-fabric.js": "secure-v1.7.9",
@ -49,7 +49,8 @@
"saferphore": "^0.0.1",
"jszip": "Stuk/jszip#^3.1.5",
"requirejs-plugins": "^1.0.3",
"seiyria-bootstrap-slider": "^10.6.2"
"dragula.js": "3.7.2",
"MathJax": "3.0.5"
},
"resolutions": {
"bootstrap": "^v4.0.0",

View File

@ -89,6 +89,14 @@ module.exports = {
*/
//httpSafePort: 3001,
/* CryptPad will launch a child process for every core available
* in order to perform CPU-intensive tasks in parallel.
* Some host environments may have a very large number of cores available
* or you may want to limit how much computing power CryptPad can take.
* If so, set 'maxWorkers' to a positive integer.
*/
// maxWorkers: 4,
/* =====================
* Admin
* ===================== */

View File

@ -1,27 +0,0 @@
#!/bin/sh
# Creating customize folder
mkdir -p customize
# Copying default config
mkdir -p cfg
[ ! -f cfg/config.js ] && echo "Creating config.js" && cp config/config.example.js cfg/config.js
# Linking config.js
[ ! -L config/config.js ] && echo "Linking config.js" && ln -s ../cfg/config.js config/config.js
# Thanks to http://stackoverflow.com/a/10467453
sedeasy() {
sed -i "s/$1/$(echo $2 | sed -e 's/[\/&]/\\&/g')/g" $3
}
# Configure
[ -n "$STORAGE" ] && echo "Using storage adapter: $STORAGE" \
&& sedeasy "storage: [^,]*," "storage: ${STORAGE}," cfg/config.js
[ -n "$LOG_TO_STDOUT" ] && echo "Logging to stdout: $LOG_TO_STDOUT" \
&& sedeasy "logToStdout: [^,]*," "logToStdout: ${LOG_TO_STDOUT}," cfg/config.js
export FRESH=1
exec node ./server.js

View File

@ -9,5 +9,8 @@ define(['/common/application_config_internal.js'], function (AppConfig) {
// Example: If you want to remove the survey link in the menu:
// AppConfig.surveyURL = "";
// To inform users of the support ticket panel which languages your admins speak:
//AppConfig.supportLanguages = [ 'en', 'fr' ];
return AppConfig;
});

View File

@ -10,7 +10,7 @@ CKEDITOR.editorConfig = function( config ) {
// document itself and causes problems when it's sent across the wire and reflected back
config.removePlugins= 'resize,elementspath';
config.resize_enabled= false; //bottom-bar
config.extraPlugins= 'autolink,colorbutton,colordialog,font,indentblock,justify,mediatag,print,blockbase64,mathjax,wordcount';
config.extraPlugins= 'autolink,colorbutton,colordialog,font,indentblock,justify,mediatag,print,blockbase64,mathjax,wordcount,comments';
config.toolbarGroups= [
// {"name":"clipboard","groups":["clipboard","undo"]},
//{"name":"editing","groups":["find","selection"]},

View File

@ -26,4 +26,7 @@
<glyph unicode="&#xe910;" glyph-name="new-template" d="M840.764 886.152h-655.119c-35.33 0-63.97-28.64-63.97-63.97v0-787.637c0-35.33 28.64-63.97 63.97-63.97v0h655.119c35.33 0 63.97 28.64 63.97 63.97v0 787.637c0 35.33-28.64 63.97-63.97 63.97v0zM844.499 34.545c0-2.063-1.672-3.735-3.735-3.735v0h-655.119c-2.063 0-3.735 1.672-3.735 3.735v0 787.637c0 2.063 1.672 3.735 3.735 3.735h655.119c2.063 0 3.735-1.672 3.735-3.735v0zM643.915 466.071h-93.365v93.003c0 10.313-8.36 18.673-18.673 18.673v0h-37.346c-0.036 0-0.078 0-0.121 0-10.246 0-18.552-8.306-18.552-18.552 0-0.042 0-0.085 0-0.127v0.006-93.003h-93.365c-0.036 0-0.078 0-0.121 0-10.246 0-18.552-8.306-18.552-18.552 0-0.042 0-0.085 0-0.127v0.006-37.346c0-10.313 8.36-18.673 18.673-18.673v0h93.365v-93.967c0-0.036 0-0.078 0-0.121 0-10.246 8.306-18.552 18.552-18.552 0.042 0 0.085 0 0.127 0h37.34c10.313 0 18.673 8.36 18.673 18.673v0 93.606h93.365c10.285 0.068 18.605 8.388 18.673 18.666v37.352c0.002 0.108 0.003 0.234 0.003 0.361 0 10.313-8.36 18.673-18.673 18.673-0.001 0-0.002 0-0.004 0v0z" />
<glyph unicode="&#xe911;" glyph-name="palette" d="M408.6 950c-198.8-38.8-359-198.6-398.2-396.8-74-374 263.4-652.8 517.6-613.4 82.4 12.8 122.8 109.2 85 183.4-46.2 90.8 19.8 196.8 121.8 196.8h159.4c71.6 0 129.6 59.2 129.8 130.6-1 315.2-287.8 563.2-615.4 499.4zM192 320c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64zM256 576c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64zM512 704c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64zM768 576c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64z" />
<glyph unicode="&#xe912;" glyph-name="folder-upload" d="M829.44 727.251h-296.84l-47.104 62.886c-25.923 34.066-66.406 55.898-111.999 56.139h-178.938c-77.457-0.137-140.211-62.891-140.348-140.335v-515.868c0.137-77.457 62.891-140.211 140.335-140.348h634.893c77.457 0.137 140.211 62.891 140.348 140.335v396.482c0 0.036 0 0.078 0 0.121 0 77.561-62.807 140.452-140.335 140.589h-0.013zM911.119 190.072c-0.068-45.083-36.597-81.611-81.673-81.679h-634.887c-45.083 0.068-81.611 36.597-81.679 81.673v515.862c0.068 45.083 36.597 81.611 81.673 81.679h178.906c26.48-0.030 50.004-12.656 64.908-32.207l0.146-0.199 47.104-62.765 17.709-24.094h326.114c45.125-0.069 81.679-36.665 81.679-81.799 0 0 0 0 0 0v0zM562.838 166.883h-102.039v203.957h-72.523l123.723 214.076 123.723-214.076h-72.885v-203.957z" />
<glyph unicode="&#xe913;" glyph-name="add-bottom" d="M108.793 271.501c-15.312 0-27.996 12.684-27.996 27.996v55.992c0 15.312 12.684 28.006 27.996 28.006h225.414v-111.993zM108.793 495.478c-15.312 0-27.996 12.694-27.996 28.006v55.992c0 15.312 12.684 27.996 27.996 27.996h403.491v-111.993zM108.793 719.465c-15.312 0-27.996 12.694-27.996 28.006v55.992c0 15.312 12.684 27.996 27.996 27.996h615.968c15.312 0 27.996-12.684 27.996-27.996v-55.992c0-15.312-12.684-28.006-27.996-28.006zM943.202 287.839c0-19.465-15.792-35.257-35.258-35.257h-152.782v-152.782c0-19.465-15.792-35.257-35.258-35.257h-70.515c-19.465 0-35.257 15.792-35.257 35.257v152.782h-152.782c-19.465 0-35.257 15.792-35.257 35.257v70.515c0 19.465 15.792 35.258 35.257 35.258h152.782v152.782c0 19.465 15.792 35.258 35.257 35.258h70.515c19.465 0 35.258-15.792 35.258-35.258v-152.782h152.782c19.465 0 35.258-15.792 35.258-35.258z" />
<glyph unicode="&#xe914;" glyph-name="add-top" d="M108.793 624.499c-15.312 0-27.996-12.684-27.996-27.996v-55.992c0-15.312 12.684-28.006 27.996-28.006h225.414v111.993zM108.793 400.522c-15.312 0-27.996-12.694-27.996-28.006v-55.992c0-15.312 12.684-27.996 27.996-27.996h403.491v111.993zM108.793 176.535c-15.312 0-27.996-12.694-27.996-28.006v-55.992c0-15.312 12.684-27.996 27.996-27.996h615.968c15.312 0 27.996 12.684 27.996 27.996v55.992c0 15.312-12.684 28.006-27.996 28.006zM943.202 608.161c0 19.465-15.792 35.257-35.258 35.257h-152.782v152.782c0 19.465-15.792 35.257-35.258 35.257h-70.515c-19.465 0-35.257-15.792-35.257-35.257v-152.782h-152.782c-19.465 0-35.257-15.792-35.257-35.257v-70.515c0-19.465 15.792-35.258 35.257-35.258h152.782v-152.782c0-19.465 15.792-35.258 35.257-35.258h70.515c19.465 0 35.258 15.792 35.258 35.258v152.782h152.782c19.465 0 35.258 15.792 35.258 35.258z" />
<glyph unicode="&#xe915;" glyph-name="destroy" d="M193.049 938.213c-25.264 0-45.745-20.492-45.745-45.757v-443.97h-67.453c-26.649-0.003-48.252-21.606-48.255-48.255 0.023-26.635 21.62-48.216 48.255-48.219h157.56l-80.102-86.481 86.481-94.352-86.396-94.23 109.353-119.164 39.535 49.965-63.439 69.247 86.347 94.181-86.262 94.085 80.344 86.748h171.688l-80.102-86.481 86.481-94.352-86.347-94.23 109.305-119.164 39.535 49.965-63.439 69.247 86.347 94.181-86.262 94.085 80.344 86.748h171.688l-80.102-86.481 86.529-94.352-86.396-94.23 109.341-119.164 39.499 49.965-63.451 69.199 86.396 94.23-86.299 94.133 80.344 86.699h105.763c26.64-0.004 48.244 21.579 48.267 48.219-0.003 26.654-21.613 48.259-48.267 48.255h-64.663v230.418c0 25.264-14.779 60.536-32.417 78.174l-148.718 148.719c-17.637 17.637-52.909 32.417-78.173 32.417zM208.317 877.2h366.078v-198.296c0-25.264 20.505-45.769 45.769-45.769h198.296v-184.65h-610.143zM635.409 873.392c8.104-2.86 16.213-7.154 19.549-10.49l149.204-149.204c3.337-3.336 7.63-11.446 10.49-19.549h-179.243z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,11 +1,12 @@
@font-face {
font-family: 'cptools';
src:
url('fonts/cptools.ttf?cljhos') format('truetype'),
url('fonts/cptools.woff?cljhos') format('woff'),
url('fonts/cptools.svg?cljhos#cptools') format('svg');
url('fonts/cptools.ttf?5ntnhs') format('truetype'),
url('fonts/cptools.woff?5ntnhs') format('woff'),
url('fonts/cptools.svg?5ntnhs#cptools') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
.cptools {
@ -24,6 +25,15 @@
-moz-osx-font-smoothing: grayscale;
}
.cptools-destroy:before {
content: "\e915";
}
.cptools-add-bottom:before {
content: "\e913";
}
.cptools-add-top:before {
content: "\e914";
}
.cptools-folder-upload:before {
content: "\e912";
}

View File

@ -341,6 +341,11 @@ define([
if (shouldImport) {
setMergeAnonDrive();
}
var l = Util.find(rt.proxy, ['settings', 'general', 'language']);
var LS_LANG = "CRYPTPAD_LANG";
if (l) {
localStorage.setItem(LS_LANG, l);
}
return void LocalStore.login(userHash, uname, function () {
cb(void 0, res);
});
@ -357,7 +362,7 @@ define([
if (shouldImport) {
setMergeAnonDrive();
} else {
proxy.version = 6;
proxy.version = 10;
}
Feedback.send('REGISTRATION', true);

View File

@ -61,6 +61,15 @@ define([
var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ?
'/imprint.html' : AppConfig.imprint);
Pages.versionString = "CryptPad v3.20.1 (UplandMoa's revenge)";
// used for the about menu
Pages.imprintLink = AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined;
Pages.privacyLink = footLink(AppConfig.privacy, 'privacy');
Pages.githubLink = footLink('https://github.com/xwiki-labs/cryptpad', null, 'GitHub');
Pages.faqLink = footLink('/faq.html', 'faq_link');
Pages.infopageFooter = function () {
return h('footer', [
h('div.container', [
@ -71,24 +80,14 @@ define([
languageSelector()
])
], ''),
/*footerCol('footer_applications', [
footLink('/drive/', 'main_drive'),
footLink('/pad/', 'main_richText'),
footLink('/code/', 'main_code'),
footLink('/slide/', 'main_slide'),
footLink('/poll/', 'main_poll'),
footLink('/kanban/', 'main_kanban'),
footLink('/whiteboard/', null, Msg.type.whiteboard)
]),*/
footerCol('footer_product', [
footLink('https://cryptpad.fr/what-is-cryptpad.html', 'topbar_whatIsCryptpad'),
footLink('/faq.html', 'faq_link'),
footLink('https://github.com/xwiki-labs/cryptpad', null, 'GitHub'),
footLink('/what-is-cryptpad.html', 'topbar_whatIsCryptpad'),
Pages.faqLink,
Pages.githubLink,
footLink('https://opencollective.com/cryptpad/contribute/', 'footer_donate'),
]),
footerCol('footer_aboutUs', [
/*footLink('https://blog.cryptpad.fr', 'blog'),
footLink('https://labs.xwiki.com', null, 'XWiki Labs'),*/
/*footLink('https://blog.cryptpad.fr', 'blog'), */
footLink('http://www.xwiki.com', null, 'XWiki SAS'),
footLink('https://www.open-paas.org', null, 'OpenPaaS'),
footLink('/about.html', 'footer_team'),
@ -96,18 +95,12 @@ define([
]),
footerCol('footer_legal', [
footLink('/terms.html', 'footer_tos'),
footLink('/privacy.html', 'privacy'),
AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined,
Pages.privacyLink,
Pages.imprintLink,
]),
/*footerCol('footer_contact', [
footLink('https://riot.im/app/#/room/#cryptpad:matrix.org', null, 'Chat'),
footLink('https://twitter.com/cryptpad', null, 'Twitter'),
footLink('https://github.com/xwiki-labs/cryptpad', null, 'GitHub'),
footLink('/contact.html', null, 'Email')
])*/
])
]),
h('div.cp-version-footer', "CryptPad v3.15.0 (PigFootedBandicoot)")
h('div.cp-version-footer', Pages.versionString)
]);
};
@ -147,13 +140,9 @@ define([
h('a.navbar-brand', { href: '/index.html'}),
button,
h('div.collapse.navbar-collapse.justify-content-end#menuCollapse', [
//h('a.nav-item.nav-link', { href: '/what-is-cryptpad.html'}, Msg.topbar_whatIsCryptpad), // Moved the FAQ
//h('a.nav-item.nav-link', { href: '/faq.html'}, Msg.faq_link),
h('a.nav-item.nav-link', { href: 'https://blog.cryptpad.fr/'}, Msg.blog),
h('a.nav-item.nav-link', { href: '/features.html'}, Msg.pricing),
h('a.nav-item.nav-link', { href: '/privacy.html'}, Msg.privacy),
//h('a.nav-item.nav-link', { href: '/contact.html'}, Msg.contact),
//h('a.nav-item.nav-link', { href: '/about.html'}, Msg.about),
].concat(rightLinks))
);
};

View File

@ -4,8 +4,9 @@ define([
'/customize/messages.js',
'/customize/application_config.js',
'/common/outer/local-store.js',
'/customize/pages.js'
], function ($, h, Msg, AppConfig, LocalStore, Pages) {
'/customize/pages.js',
'/api/config',
], function ($, h, Msg, AppConfig, LocalStore, Pages, Config) {
var origin = encodeURIComponent(window.location.hostname);
var accounts = {
donateURL: 'https://accounts.cryptpad.fr/#/donate?on=' + origin,
@ -29,15 +30,8 @@ define([
sessionStorage.redirectTo = '/features.html';
window.location.href = '/login/';
});*/
return h('div#cp-main', [
Pages.infopageTopbar(),
h('div.container-fluid.cp_cont_features',[
h('div.container',[
h('center', h('h1', Msg.features_title)),
]),
]),
h('div.container',[
h('div.row.cp-container.cp-features-web.justify-content-sm-center',[
var anonymousFeatures =
h('div.col-12.col-sm-4.cp-anon-user',[
h('div.card',[
h('div.card-body',[
@ -59,7 +53,8 @@ define([
})
),
]),
]),
]);
var registeredFeatures =
h('div.col-12.col-sm-4.cp-regis-user',[
h('div.card',[
h('div.card-body',[
@ -89,7 +84,8 @@ define([
h('div.cp-note', Msg.features_f_register_note)
]),
]),
]),
]);
var premiumFeatures =
h('div.col-12.col-sm-4.cp-anon-user',[
h('div.card',[
h('div.card-body',[
@ -120,8 +116,21 @@ define([
LocalStore.isLoggedIn() ? undefined : h('div.cp-note', Msg.features_f_subscribe_note)
]),
]),
]);
var availableFeatures =
Config.allowSubscriptions ?
[anonymousFeatures, registeredFeatures, premiumFeatures] :
[anonymousFeatures, registeredFeatures];
return h('div#cp-main', [
Pages.infopageTopbar(),
h('div.container-fluid.cp_cont_features',[
h('div.container',[
h('center', h('h1', Msg.features_title)),
]),
]),
h('div.container', [
h('div.row.cp-container.cp-features-web.justify-content-sm-center', availableFeatures),
]),
Pages.infopageFooter()
]);

View File

@ -3,7 +3,6 @@
@import (reference) "./variables.less";
@import (reference) "./avatar.less";
@import (reference) "./tools.less";
@import (reference) "./buttons.less";
.alertify_main() {
--LessLoader_require: LessLoader_currentFile();
@ -228,16 +227,7 @@
}
}
::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: @cryptpad_color_grey;
opacity: 1; /* Firefox */
}
:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: @cryptpad_color_grey;
}
::-ms-input-placeholder { /* Microsoft Edge */
color: @cryptpad_color_grey;
}
.tools_placeholder-color(@cryptpad_color_grey);
span.cp-password-container {
display: flex;
@ -272,7 +262,6 @@
}
}
.buttons_main();
input:not(.form-control), textarea {
margin-bottom: 15px;
}

View File

@ -21,7 +21,6 @@
}
* {
visibility: hidden;
height: auto;
max-height: none;
}
.cp-app-slide-viewer #cp-app-slide-print {

View File

@ -68,36 +68,40 @@
@colortheme_dropdown-bg-active: #e8e8e8;
// Apps, these colors are used for customizing the toolbar for the apps.
@colortheme_toolbar-warn: @colortheme_alertify-red;
@colortheme_pad-toolbar-bg: #eeeeee;
@colortheme_help-bg: #ddd;
@colortheme_userlist-bg: #eee;
@colortheme_pad-chat-bg: #AAA;
@colortheme_pad-bg: #1c4fa0;
@colortheme_pad-bg: #256ad5;
@colortheme_pad-color: #fff;
@colortheme_pad-toolbar-bg: #c1e7ff;
@colortheme_pad-warn: #ffae00;
@colortheme_slide-bg: #e57614;
@colortheme_slide-color: #fff;
@colortheme_slide-warn: #005868;
@colortheme_code-bg: #ffae00;
@colortheme_code-bg: #EAA000;
@colortheme_code-color: #000;
@colortheme_code-warn: #9A37F7;
@colortheme_poll-bg: #006304;
@colortheme_poll-bg: #2c9e98;
@colortheme_poll-color: #fff;
@colortheme_poll-help-bg: #bbffbb;
@colortheme_poll-th-bg: #005bef;
@colortheme_poll-th-fg: #fff;
@colortheme_poll-warn: #ffade3;
@colortheme_whiteboard-bg: #800080;
@colortheme_whiteboard-bg: #a72ba7;
@colortheme_whiteboard-color: #fff;
@colortheme_whiteboard-warn: #ffae00;
@colortheme_drive-bg: #0087ff;
@colortheme_drive-bg: #0087FF;
@colortheme_drive-color: #fff;
@colortheme_drive-warn: #cd2532;
@colortheme_teams-bg: #0b0061;
@colortheme_teams-bg: #4a3bbd;
@colortheme_teams-color: #fff;
@colortheme_teams-warn: #cd2532;
@ -121,8 +125,8 @@
@colortheme_profile-color: #fff;
@colortheme_profile-warn: #cd2532;
@colortheme_todo-bg: #7bccd1;
@colortheme_todo-color: #000;
@colortheme_todo-bg: #999;
@colortheme_todo-color: #3F4141;
@colortheme_todo-warn: #cd2532;
@colortheme_oodoc-bg: #5170B5;
@ -141,15 +145,15 @@
@colortheme_kanban-color: #000;
@colortheme_kanban-warn: #e6385d;
@colortheme_admin-bg: #7c0404;
@colortheme_admin-bg: #0087ff;
@colortheme_admin-color: #FFF;
@colortheme_admin-warn: #ffae00;
@colortheme_notifications-bg: #4ae397;
@colortheme_notifications-bg: #0087ff;
@colortheme_notifications-color: #000;
@colortheme_notifications-warn: #e34a85;
@colortheme_support-bg: #42d1f4;
@colortheme_support-bg: #0087ff;
@colortheme_support-color: #000;
@colortheme_support-warn: #9A37F7;

View File

@ -0,0 +1,157 @@
@import (reference) "./colortheme-all.less";
@import (reference) "./variables.less";
@import (reference) "./avatar.less";
@import (reference) "./tools.less";
.comments_main() {
@data-color: #888;
overflow-y: auto;
color: @cryptpad_text_col;
&:empty {
display: none !important;
}
&.cp-comments-readonly {
.cp-comment-actions {
display: none !important;
}
.cp-comment-form {
display: none !important;
}
.cp-comment-edit {
display: none !important;
}
}
.cp-comment-reply {
margin-left: 30px;
}
.cp-comment-deleted {
background: white;
font-size: 14px;
padding: 5px;
}
.cp-comment-form {
&:not(:last-child) {
padding: 5px;
margin-bottom: 10px;
}
}
.cp-comment-form-input {
.avatar_main(40px);
.cp-avatar {
border: 1px solid transparent;
}
display: flex;
align-items: flex-start;
div.cp-textarea {
flex: 1;
min-height: 52px; // 22px per line + 8 (padding+border)
height: unset !important;
max-height: 140px; // 6 lines
padding: 3px 5px;
}
margin-bottom: 5px;
}
.cp-comment-form-actions {
text-align: right;
margin-left: -30px;
button:not(:last-child) {
margin-right: 5px;
}
}
.cp-comment-container {
outline: none;
&:not(:focus) {
cursor: pointer;
.tools_unselectable();
}
//&:not(:last-child) {
// margin-bottom: 10px;
//}
.cp-comment-form {
margin-top: 5px;
}
padding: 5px;
&:nth-child(2) {
margin-top: 10px;
};
&:last-child {
margin-bottom: 10px;
}
}
.cp-comment {
&:not(:first-child) {
margin-top: 5px;
}
}
.cp-comment-header {
height: 40px;
align-items: center;
display: flex;
background-color: white;
position: relative;
padding: 5px;
box-sizing: content-box;
.avatar_main(40px);
.cp-comment-metadata {
flex: 1;
display: flex;
flex-flow: column;
margin-left: 5px;
.cp-comment-time {
font-size: 13px;
color: @data-color;
}
}
.cp-comment-edit {
cursor: pointer;
outline: none;
position: absolute;
right: 0;
top: 0;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
&:hover {
color: lighten(@cryptpad_text_col, 10%);
}
}
}
.cp-comment-content {
background-color: white;
padding: 0px 5px 5px 5px;
white-space: pre-wrap;
word-break: break-word;
}
.cp-comment-edited {
background-color: white;
font-size: 13px;
color: @data-color;
padding: 0 5px;
}
.cp-comment-actions {
display: none;
text-align: right;
margin-top: 5px;
button {
margin-bottom: 0 !important;
}
button:not(:last-child) {
margin-right: 5px;
}
}
.cp-comment-active {
background-color: rgba(0,0,0,0.2);
.cp-comment-actions {
display: block;
}
}
}

View File

@ -3,6 +3,7 @@
@import (reference) "./tools.less";
@import (reference) "./limit-bar.less";
@import (reference) "./tokenfield.less";
@import (reference) "./dropdown.less";
.drive_main() {
--LessLoader_require: LessLoader_currentFile();
@ -11,6 +12,10 @@
.limit-bar_main();
.tokenfield_main();
@colortheme_drive-bg-light: lighten(@colortheme_drive-bg, 30%);
@colortheme_drive-bg-active: lighten(@colortheme_drive-bg, 20%);
@colortheme_drive-color: @cryptpad_text_col;
@drive_hover: #eee;
@drive_hover-light: lighten(@drive_hover, 20%);
@drive_info-box-bg: #d2e1f2;
@ -39,10 +44,11 @@
}
/* local mixins */
@drive_icon-margin: 10px;
.drive_fileIcon {
li {
display: inline-block;
margin: 10px 10px;
margin: @drive_icon-margin;
width: 140px;
height: 140px;
text-align: center;
@ -102,12 +108,14 @@
.cp-app-drive-container {
flex: 1;
overflow-x: auto;
width: 100%;
display: flex;
flex-flow: row;
min-height: 0;
min-width: 0;
@media screen and (max-width: @browser_media-medium-screen) {
display: block;
overflow-y: auto;
#cp-app-drive-toolbar {
.path .element {
display: none;
@ -151,6 +159,10 @@
user-select: none;
}
.cp-app-drive-element-restricted {
color: #939393;
}
.cp-app-drive-element-droppable {
background-color: @drive_droppable-bg;
color: #222;
@ -159,7 +171,6 @@
.cp-app-drive-element-selected {
background: @drive_selected-bg !important;
color: #eee;
margin: -1px;
.fa-minus-square-o, .fa-plus-square-o {
color: @colortheme_sidebar-left-fg;
}
@ -169,7 +180,6 @@
border: 1px dotted #bbb;
background: #AAA;
color: #ddd;
margin: -1px;
.fa-minus-square-o, .fa-plus-square-o {
color: @colortheme_sidebar-left-fg;
}
@ -211,7 +221,54 @@
}
#cp-app-drive-search {
display: inline-flex;
align-items: center;
max-width: 400px;
font-size: 30px;
margin: 15px;
input {
background: transparent;
color: @colortheme_drive-color;
.tools_placeholder-color(@cryptpad_color_grey);
outline-width: 0px;
border-radius: 0;
width: 100%;
border: 0;
border-bottom: 3px solid @colortheme_drive-color;
margin: 0 5px;
flex: 1;
min-width: 0;
&:focus {
outline-width: 0px;
}
&.cp-app-drive-search-active {
& ~ .cp-app-drive-search-cancel {
visibility: visible;
}
}
}
.cp-app-drive-search-cancel {
visibility: hidden;
cursor: pointer;
}
.cp-app-drive-tree-search-icon, .cp-app-drive-search-cancel {
color: @colortheme_drive-color;
}
}
.cp-app-drive-search-spinner {
display: inline-flex;
color: @colortheme_drive-color;
font-size: 40px;
align-items: center;
justify-content: center;
}
.cp-app-drive-search-noresult {
font-size: 30px;
padding: 15px;
font-style: italic;
color: @cryptpad_color_grey;
}
/* TREE */
@ -233,6 +290,22 @@
display: flex;
flex-flow: column;
max-height: 100%;
position: relative;
.cp-close-button {
position: absolute;
cursor: pointer;
right: 5px;
top: 5px;
font-size: 18px;
padding: 3px;
margin: 0;
border: 0;
background-color: transparent;
outline: none;
&:hover {
color: #000;
}
}
.cp-app-drive-tree-categories-container {
flex: 1;
overflow: auto;
@ -256,6 +329,7 @@
padding: 0 10px;
border: 0;
color: lighten(@colortheme_sidebar-left-fg, 40%);
height: auto;
}
& > span.cp-app-drive-element-row {
overflow: hidden;
@ -305,51 +379,6 @@
.cp-limit-container {
margin-top: 5px;
}
#cp-app-drive-tree-search {
text-align: center;
padding: 0;
position: relative;
display: flex;
background: lighten(@colortheme_drive-bg, 8%);
border-right: 1px solid lighten(@colortheme_drive-bg, 16%);
input {
background: transparent;
color: @colortheme_drive-color;
.tools_placeholder-color(@colortheme_drive-color);
outline-width: 0px;
border-radius: 0;
width: 100%;
//border: 1px solid #ccc;
border: 0;
//border-right: 0;
height: @variables_bar-height;
padding: 0 5px;
padding-left: 45px;
flex: 1;
min-width: 0;
&:focus {
outline-width: 0px;
}
&.cp-app-drive-search-active {
& ~ .cp-app-drive-tree-search-icon {
display: none;
}
& ~ .cp-app-drive-search-cancel {
display: inline-block;
}
}
}
.cp-app-drive-search-cancel {
display: none;
cursor: pointer;
}
.cp-app-drive-tree-search-icon, .cp-app-drive-search-cancel {
color: @colortheme_drive-color;
position: absolute;
left: 20px; // TODO align with drive categories
top: 8px;
}
}
.fa.cp-app-drive-icon-expcol {
margin-left: -10px;
font-size: 14px;
@ -434,10 +463,8 @@
#cp-app-drive-content-container {
display: flex;
flex-flow: column;
flex: 1;
// Needed to avoid the folder's path to overflows
// https://stackoverflow.com/questions/38223879/white-space-nowrap-breaks-flexbox-layout
// min-width: 0;
flex: 1 1 100%;
min-width: 0;
}
#cp-app-drive-content {
box-sizing: border-box;
@ -462,6 +489,7 @@
margin-top: 10px;
}
.cp-app-drive-content-info-box {
order: 10;
line-height: 2em;
padding: 0.25em 0.75em;
margin: 1em;
@ -484,35 +512,12 @@
}
}
#cp-app-drive-content-folder {
order: 20;
li {
&.cp-app-drive-search-result {
border-bottom: 1px solid @drive_info-box-border;
display: block;
&:hover {
background-color: initial;
}
table {
width: 100%;
.cp-app-drive-search-label2 {
width: 150px;
font-size: 15px;
text-align: right;
padding-right: 15px;
}
.cp-app-drive-search-opendir {
display: flex;
justify-content: space-between;
a {
cursor: pointer;
color: #41b7d8;
&:hover {
color: #014c8c;
text-decoration: underline;
}
}
}
height: 2 * @variables_bar-height;
.cp-app-drive-search-path {
font-style: italic;
.cp-app-drive-path-inner {
display: flex;
flex-flow: row-reverse wrap-reverse;
@ -525,28 +530,22 @@
word-wrap: break-word;
max-width: 100%;
}
}
}
.cp-app-drive-search-title {
font-weight: bold;
.cp-app-drive-path-clickable {
cursor: pointer;
&:hover {
background-color: @drive_hover;
background-color: #eee;
}
}
.cp-app-drive-search-col2 {
width: 250px;
}
td.cp-app-drive-search-icon {
width: 50px;
font-size: 40px;
}
font-size: 12px;
line-height: 16px;
color: @drive_table-header-fg;
}
}
&.cp-app-drive-tags-list {
width: 100%;
table {
margin: 10px 50px;
margin: 10px;
width: ~"calc(100% - 100px)";
table-layout: fixed;
td, th {
@ -566,8 +565,26 @@
.cp-app-drive-element {
.cp-app-drive-element-truncated { display: none; }
}
.cp-app-drive-new-ghost {
cursor: pointer;
opacity: 0.5;
padding: 0;
align-items: center;
justify-content: center;
display: inline-flex;
&:hover {
opacity: 0.7;
}
.fa, .cptools {
cursor: pointer;
}
}
div.cp-app-drive-content-grid {
padding: 20px;
padding: 1em;
ul {
margin: -(@drive_icon-margin);
}
.drive_fileIcon;
li {
&.cp-app-drive-element {
@ -586,6 +603,7 @@
border-radius: 0;
border: 1px solid #ddd;
font-size: 14px;
height: auto;
}
.cp-app-drive-element-state {
position: absolute;
@ -609,27 +627,17 @@
}
}
}
.cp-app-drive-element-list {
display: none;
}
.cp-app-drive-new-ghost {
cursor: pointer;
opacity: 0.5;
padding: 0;
flex-flow: column;
align-items: center;
justify-content: center;
display: inline-flex;
&:hover {
opacity: 0.7;
}
.fa, .cptools {
cursor: pointer;
font-size: 90px;
margin-top: 5px;
margin-bottom: 0;
}
}
.cp-app-drive-element-list {
display: none;
}
}
.cp-app-drive-content-list {
@ -637,22 +645,77 @@
display: none;
}
// Make it act as a table!
padding-left: 20px;
.cp-app-drive-new-ghost {
padding: 0 5px;
margin-top: 20px;
}
padding-left: 10px;
ul {
display: table;
width: 100%;
padding: 0px 10px;
}
li {
display: table-row;
display: flex;
input {
border: 1px solid #ddd;
margin: 0;
padding: 0 4px;
flex: 1;
min-width: 0;
height: auto;
}
&> span {
padding: 0 5px;
display: table-cell;
display: inline-block;
white-space: nowrap;
&:first-child, &.cp-icon {
min-width: 20px;
text-align: center;
font-size: 18px;
line-height: 32px;
padding: 0;
}
&.cp-app-drive-element-name {
flex: 1;
min-width: 0;
text-align: left;
text-overflow: ellipsis;
overflow: hidden;
}
&.cp-app-drive-element-state {
min-width: 40px;
text-align: right;
display: flex;
align-items: center;
justify-content: end;
height: @variables_bar-height;
.fa, .cptools {
&:not(:last-child) {
margin-right: 2px;
}
}
}
&.cp-app-drive-element-list {
width: 120px;
}
&.cp-app-drive-element-sort {
display: none;
.dropdown_main();
.cp-dropdown-content a {
display: block !important;
.sortdesc {
order: 2;
}
}
button {
color: inherit;
background: none;
border: none;
.fa {
margin-right: 5px;
}
}
}
}
&:not(.cp-app-drive-element-header) {
height: @variables_bar-height;
@ -663,6 +726,7 @@
height: 1.5 * @variables_bar-height;
line-height: 1.5 * @variables_bar-height;
span {
line-height: 1.5 * @variables_bar-height;
position: absolute;
}
}
@ -674,14 +738,11 @@
text-align: left;
}
&.sortasc, &.sortdesc {
float: right;
margin-right: 3px;
}
}
&> span {
padding: 15px 5px;
&.cp-app-drive-sort-active {
font-weight: bold;
}
padding: 14px 5px;
&.cp-app-drive-sort-clickable {
cursor: pointer;
&:hover {
@ -689,6 +750,9 @@
}
}
}
.cp-app-drive-element-state {
display: none !important;
}
}
}
.cp-app-drive-element {
@ -696,27 +760,98 @@
overflow: hidden;
white-space: nowrap;
box-sizing: border-box;
&.cp-app-drive-element-state {
.fa, .cptools {
&:not(:last-child) {
margin-right: 5px;
}
}
@media screen and (max-width: 840px) {
.cp-app-drive-element-type, .cp-app-drive-element-filler {
display: none !important;
}
.cp-app-drive-element-sort {
display: block !important;
}
}
@media screen and (max-width: 740px) {
.cp-app-drive-element-folders, .cp-app-drive-element-ctime {
display: none !important;
}
}
@media screen and (max-width: 670px) {
.cp-app-drive-element-files, .cp-app-drive-element-atime {
display: none !important;
}
.cp-app-drive-element-header .cp-app-drive-element-state {
display: none !important;
}
}
}
&.cp-app-drive-content-icon, &.cp-app-drive-element-state, &.cp-icon {
width: 30px;
& > .cp-app-drive-path {
order: 1;
width: 100%;
height: @variables_bar-height;
line-height: @variables_bar-height;
cursor: default;
width: auto;
overflow: hidden;
white-space: nowrap;
flex-shrink: 0;
min-width: 50px;
max-width: 100%;
text-align: left;
display: flex;
flex-direction: row;
.cp-app-drive-path-inner {
display: flex;
flex-flow: row-reverse;
flex-grow: 1;
justify-content: flex-end;
padding-left: 10px;
.cp-app-drive-path-element {
.tools_unselectable();
display: inline-block;
flex-shrink: 0;
max-width: 100%;
height: @variables_bar-height;
line-height: @variables_bar-height;
font-size: @colortheme_app-font-size;
padding: 0 5px;
border: 0;
//background: @colortheme_drive-bg-active;
color: @colortheme_drive-color;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.15s;
&:first-child {
flex-shrink: 1;
}
&.cp-app-drive-element-type, &.cp-app-drive-element-atime, &.cp-app-drive-element-ctime {
width: 175px;
&.cp-app-drive-path-separator {
color: #ccc;
cursor: default;
}
&.cp-app-drive-element-title {
width: 250px;
@media screen and (max-width: 1200px) {
display: none;
&.cp-app-drive-path-collapse {
position: relative;
}
&.cp-app-drive-element-droppable {
background-color: @drive_droppable-bg;
}
&.cp-app-drive-path-clickable {
cursor: pointer;
&:hover {
&:not(.cp-app-drive-path-separator) {
text-decoration: underline;
}
& ~ .cp-app-drive-path-element:not(.cp-app-drive-path-separator) {
text-decoration: underline;
}
}
&.cp-app-drive-element-folders, &.cp-app-drive-element-files {
width: 150px;
}
}
}
@ -793,199 +928,28 @@
}
/* Toolbar */
#cp-app-drive-toolbar {
background: lighten(@colortheme_drive-bg, 8%);
color: @colortheme_drive-color;
//height: 30px;
//display: flex;
//flex-flow: row;
z-index: 100;
box-sizing: border-box;
height: @variables_bar-height;
padding: 0;
display: flex;
flex-flow: row;
* {
outline-width: 0;
&:focus {
outline-width: 0;
}
}
.cp-toolbar-icon-history {
float: right;
&.active {
background-color: rgba(0, 0, 255, 0.2);
}
.cp-toolbar-drawer-element {
display: none;
}
}
.cp-app-drive-toolbar-rightside, .cp-app-drive-toolbar-leftside {
display: inline-block;
margin: 0;
padding: 0;
.fa, .cptools {
margin: 0;
vertical-align: top;
}
button {
height: @variables_bar-height !important;
padding: 0 10px;
border: none;
border-radius: 0;
box-sizing: border-box;
background: transparent;
font-size: @colortheme_app-font-size;
color: @colortheme_drive-color;
transition: all 0.15s;
display: inline-flex;
align-items: center;
.drawer {
display: none;
}
.fa, .cptools, span {
font-size: @colortheme_app-font-size;
}
&:hover {
background: @colortheme_drive-bg;
}
&.cp-app-drive-toolbar-active {
display: none;
}
}
}
.cp-app-drive-toolbar-rightside {
float: right;
flex-shrink: 0;
& > * {
float: right;
.cp-toolbar-bottom {
.cp-toolbar-bottom-right {
.fa-history { order: 50; }
.fa-list, .fa-th-large { order: 25; }
#cp-app-drive-toolbar-context-mobile, #cp-app-drive-toolbar-contextbuttons { order: 0; }
#cp-app-drive-toolbar-context-mobile {
.fa { margin: 0 !important; }
}
#cp-app-drive-toolbar-contextbuttons {
display: inline-block;
height: 100%;
}
padding-left: 10px;
}
.cp-app-drive-toolbar-leftside {
flex-shrink: 0;
& > span {
height: 100%;
margin: 0;
}
button {
padding: 0 10px;
.fa, .cptools {
margin-right: 5px;
}
.cp-dropdown-button-title {
display: inline-flex;
height: @variables_bar-height;
display: flex;
align-items: center;
span:not(.fa):not(.cptools) {
line-height: 23px;
}
}
}
}
button {
font: @colortheme_app-font;
span {
font: @colortheme_app-font;
}
.fa, &.fa {
font-family: FontAwesome;
}
.cptools, &.cptools {
font-family: cptools;
}
}
.cp-app-drive-path {
width: 100%;
height: @variables_bar-height;
line-height: @variables_bar-height;
cursor: default;
width: auto;
overflow: hidden;
white-space: nowrap;
flex-shrink: 1;
min-width: 50px;
max-width: 100%;
text-align: left;
display: flex;
flex-direction: row;
.cp-app-drive-path-inner {
display: flex;
flex-flow: row-reverse;
flex-grow: 1;
.cp-app-drive-path-element {
display: inline-block;
flex-shrink: 0;
max-width: 100%;
height: @variables_bar-height;
line-height: @variables_bar-height;
font-size: @colortheme_app-font-size;
padding: 0 5px;
border: 0;
background: darken(@colortheme_drive-bg, 7%);
color: @colortheme_drive-color;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.15s;
cursor: pointer;
&:first-child {
flex-shrink: 1;
}
&.cp-app-drive-path-separator {
color: #ccc;
cursor: default;
}
&.cp-app-drive-path-collapse {
position: relative;
}
&.cp-app-drive-element-droppable {
background-color: @drive_droppable-bg;
}
&:not(.cp-app-drive-element-droppable):hover {
&:not(.cp-app-drive-path-separator) {
background-color: darken(@colortheme_drive-bg, 15%);
text-decoration: underline;
}
& ~ .cp-app-drive-path-element {
background-color: darken(@colortheme_drive-bg, 15%);
}
& ~ .cp-app-drive-path-element:not(.cp-app-drive-path-separator) {
text-decoration: underline;
}
}
}
}
}
.cp-app-drive-toolbar-filler {
flex: 1;
}
}
#cp-app-drive-edition-state {
height: @variables_bar-height;
display: flex;
align-items: center;
justify-content: center;
background-color: lighten(@colortheme_drive-bg, 32%);
background-color: @colortheme_drive-bg-active;
color: black;
font-weight: bold;
text-transform: uppercase;
@ -1002,5 +966,11 @@
text-transform: uppercase;
cursor: default;
}
.cp-app-drive-button {
order: 15;
margin: 0 1em;
}
}

View File

@ -1,11 +1,30 @@
@import (reference) "./colortheme-all.less";
@import (reference) "./tools.less";
@import (reference) "./avatar.less";
/* The container <div> - needed to position the dropdown content */
.dropdown_main () {
--LessLoader_require: LessLoader_currentFile();
}
& {
.cp-autocomplete-value {
display: flex;
align-items: center;
.cp-avatar {
.avatar_main(30px);
padding: 1px;
}
& > span:last-child {
flex: 1;
height: 32px;
line-height: 32px;
padding: 0 10px;
}
span {
margin: 0px !important;
border: none !important;
}
}
.cp-dropdown-container {
@dropdown_font: @colortheme_app-font-size @colortheme_font;
position: relative;

View File

@ -1,7 +1,7 @@
@import (reference) "./colortheme-all.less";
@import (reference) "./variables.less";
.buttons_main() {
.forms_main() {
@alertify-fore: @colortheme_modal-fg;
@alertify-btn-fg: @alertify-fore;
@alertify-light-bg: fade(@alertify-fore, 25%);
@ -9,7 +9,7 @@
@alertify-input-bg: @colortheme_modal-input;
@alertify-input-fg: @colortheme_modal-input-fg;
input:not(.form-control), textarea {
input:not(.form-control):not([type="checkbox"]), textarea, div.cp-textarea {
// background-color: @alertify-input-fg;
color: @cryptpad_text_col;
border: 1px solid @alertify-input-bg;
@ -30,7 +30,7 @@
div.cp-alertify-type {
display: flex;
input {
margin: 0;
margin: 0 !important;
flex: 1;
min-width: 0;
}
@ -44,18 +44,29 @@
}
}
textarea {
textarea, div.cp-textarea {
padding: 8px;
&[readonly] {
overflow: hidden;
resize: none;
}
}
div.cp-textarea {
height: 60px;
width: 100%;
background-color: white;
cursor: text;
outline: none;
white-space: pre-wrap;
overflow-y: auto;
word-break: break-word;
resize: vertical;
}
div.cp-button-confirm {
display: inline-block;
button {
margin: 0;
margin: 0 !important;
}
.cp-button-timer {
height: 3px;
@ -72,7 +83,7 @@
margin-bottom: 3px !important;
}
button:not(.pure-button):not(.md-button):not(.mdl-button) {
button.btn {
background-color: @colortheme_alertify-cancel;
box-sizing: border-box;
@ -88,9 +99,12 @@
cursor: pointer;
border-radius: 0;
.fa {
.fa, .cptools {
margin-right: 0.2em;
}
.cptools {
vertical-align: middle;
}
color: @alertify-btn-fg;
border: 1px solid @alertify-btn-fg;
@ -99,7 +113,7 @@
margin: 0;
}
&:hover, &:active {
&:hover, &:active, &:focus {
background-color: lighten(@alertify-fore, 35%);
}
@ -113,7 +127,7 @@
background-color: @colortheme_alertify-red;
border-color: @colortheme_alertify-red-border;
color: @colortheme_alertify-red-color;
&:hover, &:active {
&:hover, &:active, &:focus {
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-red, 10%), lighten(@colortheme_alertify-red, 10%));
}
}
@ -121,7 +135,7 @@
&.danger-alt, &.btn-danger-alt {
border-color: @colortheme_alertify-red;
color: @colortheme_alertify-red;
&:hover, &:active {
&:hover, &:active, &:focus {
color: @colortheme_alertify-red-color;
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-red, 10%), lighten(@colortheme_alertify-red, 10%));
}
@ -131,7 +145,7 @@
background-color: @colortheme_alertify-green;
border-color: @colortheme_alertify-green-border;
color: @colortheme_alertify-green-color;
&:hover, &:active {
&:hover, &:active, &:focus {
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-green, 10%), lighten(@colortheme_alertify-green, 10%));
}
}
@ -141,7 +155,7 @@
color: @colortheme_alertify-primary-text;
border-color: @colortheme_alertify-primary-border;
font-weight: bold;
&:hover, &:active {
&:hover, &:active, &:focus {
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-primary, 10%), lighten(@colortheme_alertify-primary, 10%));
}
}
@ -149,7 +163,7 @@
&.cancel, &.btn-cancel {
border-color: @colortheme_alertify-cancel-border;
color: @colortheme_alertify-cancel-border;
&:hover, &:hover {
&:hover, &:hover, &:focus {
background-color: fade(@colortheme_alertify-cancel-border, 25%);
}
}

View File

@ -15,9 +15,15 @@
@import (reference) "./messenger.less";
@import (reference) "./cursor.less";
@import (reference) "./usergrid.less";
@import (reference) "./mentions.less";
@import (reference) "./forms.less";
@import (reference) "./modals-ui-elements.less";
.framework_main(@bg-color, @warn-color, @color) {
.framework_main(
@bg-color: @colortheme_default-bg, // color of the toolbar background
@warn-color: @colortheme_default-warn, // color of the warning text in the toolbar
@color: @colortheme_default-color, // Color of the text for the toolbar
) {
--LessLoader_require: LessLoader_currentFile();
// Set the HTML style for the apps which shouldn't have a body scrollbar
.app-noscroll_main();
@ -41,9 +47,14 @@
.tippy_main();
.checkmark_main(20px);
.password_main();
.messenger_main();
.messenger_main(
@color: @cryptpad_text_col,
@bg-color: @colortheme_pad-chat-bg
);
.cursor_main();
.usergrid_main();
.forms_main();
.mentions_main();
.creation_main(
@bg-color: @bg-color,
@color: @color
@ -79,6 +90,7 @@
.checkmark_main(20px);
.password_main();
.usergrid_main();
.forms_main();
font: @colortheme_app-font;
}

View File

@ -1,30 +1,15 @@
@import (reference) "./colortheme-all.less";
.help_vars (
@color: @colortheme_default-color,
@bg-color: @colortheme_default-bg
) {
@help-bg-color-l15: lighten(@bg-color, 15%);
@help-text-color: contrast(@help-bg-color-l15, #fff, #000); //@color;
@help-link-color: contrast(@help-bg-color-l15, lighten(spin(@bg-color, 180), 10%), darken(spin(@bg-color, 180), 10%));
}
.help_main (
@color: @colortheme_default-color,
@bg-color: @colortheme_default-bg
) {
.help_main () {
--LessLoader_require: LessLoader_currentFile();
.help_vars(@color, @bg-color);
--help-bg-color-l15: @help-bg-color-l15;
--help-text-color: @help-text-color;
--help-link-color: @help-link-color;
};
& {
.help_vars();
.cp-help-container {
position: relative;
background-color: @help-bg-color-l15;
background-color: var(--help-bg-color-l15);
background-color: @colortheme_help-bg;
max-height: 50%;
overflow-y: auto;
&.cp-help-hidden {
display: none;
}
@ -36,13 +21,11 @@
cursor: pointer;
}
.cp-help-text {
color: @help-text-color;
color: var(--help-text-color);
color: @cryptpad_text_col;
margin: 0;
padding: 5px 15px;
a {
color: @help-link-color;
color: var(--help-link-color);
color: @colortheme_link-color;
}
h1 {
font-size: 20px;

View File

@ -10,7 +10,6 @@
margin: 15px 0;
cursor: pointer;
height: @variables_bar-height;
line-height: @variables_bar-height - 10px;
.fa, .cptools {
display: inline-flex;
justify-content: center;

View File

@ -1,21 +1,32 @@
@import (reference) "./tools.less";
.markdown_main() {
@nice-grey: #f3f3f3;
@accent-grey: rgba(0, 0, 0, 0.2);
hr {
border-top: 1px solid @accent-grey;
}
blockquote {
background: #e5e5e5;
background: rgba(128,128,128,0.5);
background: @nice-grey;
background: rgba(144, 144, 144, 0.2);
padding: 10px;
border-left: 3px solid #999;
border-left: 2px solid @accent-grey;
padding-right: 0;
p { margin: 0; }
blockquote { margin: 0; }
& > *:not(:last-child) {
margin-bottom: 10px !important;
}
}
// todo ul, ol
// TOC
div.cp-md-toc {
background: #f3f3f3;
background: @nice-grey;
padding: 20px;
//float: right;
* {
margin: 5px;
margin-right: 0;
}
max-width: 100%;
min-width: 200px;
white-space: nowrap;
@ -74,6 +85,7 @@
}
}
media-tag {
cursor: pointer;
* {
max-width: 100%;
}
@ -90,9 +102,17 @@
border: 1px solid #BBB;
}
pre.mermaid {
pre.markmap {
border: 1px solid #ddd;
svg {
height: 400px;
}
}
pre[data-plugin] {
svg {
max-width: 100%;
cursor: pointer;
.tools_unselectable();
}
}
}

View File

@ -0,0 +1,32 @@
@import (reference) "./tools.less";
@import (reference) "./avatar.less";
.mentions_main() {
--LessLoader_require: LessLoader_currentFile();
}
& {
.cp-mentions {
.avatar_main(20px);
.tools_unselectable();
display: inline-flex;
align-items: center;
vertical-align: bottom;
background-color: #eee;
span.cp-mentions-name {
max-width: 150px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding: 0px 3px;
}
&.cp-mentions-clickable {
outline: none;
cursor: pointer;
&:hover {
background-color: #ddd;
}
}
}
}

View File

@ -1,11 +1,32 @@
@import (reference) './avatar.less';
@import (reference) "./colortheme-all.less";
.messenger_main() {
.messenger_vars (
@color: @colortheme_friends-color, // color of the toolbar text
@bg-color: @colortheme_friends-bg, // color of the toolbar background
) {
@msg-color: @color;
@msg-bg-color: @bg-color;
@msg-bg-color-light: lighten(@bg-color, 15%);
@msg-bg-color-lighter: lighten(@bg-color, 20%);
@msg-bg-color-dark: darken(@bg-color, 10%);
@msg-bg-color-darker: darken(@bg-color, 20%);
};
.messenger_main(
@color: @colortheme_friends-color, // color of the toolbar text
@bg-color: @colortheme_friends-bg, // color of the toolbar background
) {
--LessLoader_require: LessLoader_currentFile();
.messenger_vars(@color, @bg-color);
--msg-color: @msg-color;
--msg-bg-color: @msg-bg-color;
--msg-bg-color-light: @msg-bg-color-light;
--msg-bg-color-lighter: @msg-bg-color-lighter;
--msg-bg-color-dark: @msg-bg-color-dark;
--msg-bg-color-darker: @msg-bg-color-darker;
};
& {
@keyframes example {
@keyframes notif {
0% {
background: rgba(0,0,0,0.1);
}
@ -17,9 +38,8 @@
}
}
.messenger_vars();
@button-border: 2px;
@bg-color: @colortheme_friends-bg;
@color: @colortheme_friends-color;
@room-height: 48px;
#cp-app-contacts-container {
@ -57,7 +77,8 @@
#cp-app-contacts-friendlist {
width: 200px;
height: 100%;
background-color: lighten(@bg-color, 10%);
background-color: @msg-bg-color;
background-color: var(--msg-bg-color);
overflow-y: auto;
display: flex;
flex-flow: column;
@ -86,7 +107,7 @@
background-color: rgba(0,0,0,0.3);
}
&.cp-app-contacts-notify {
animation: example 2s ease-in-out infinite;
animation: notif 2s ease-in-out infinite;
}
}
.cp-app-contacts-remove {
@ -94,7 +115,7 @@
width: 20px;
text-align: center;
&:hover {
color: darken(@color, 20%);
color: darken(@msg-color, 20%);
}
}
@ -183,7 +204,7 @@
display: flex;
}
cursor: pointer;
color: @color;
color: @msg-color;
media-tag {
img {
color: #000;
@ -252,14 +273,16 @@
#cp-app-contacts-messaging {
flex: 1;
height: 100%;
background-color: lighten(@bg-color, 20%);
background-color: @msg-bg-color-lighter;
background-color: var(--msg-bg-color-lighter);
min-width: 0;
.cp-app-contacts-info {
padding: 20px;
}
.cp-app-contacts-header {
background-color: lighten(@bg-color, 15%);
background-color: @msg-bg-color;
background-color: var(--msg-bg-color);
padding: 0;
display: flex;
justify-content: space-between;
@ -289,7 +312,7 @@
//display: none;
.hover;
&.cp-app-contacts-faded {
color: darken(@bg-color, 5%);
color: @cryptpad_text_col;
}
}
@ -300,7 +323,8 @@
}
.cp-app-contacts-tips {
margin: 1em;
background-color: lighten(@bg-color, 15%);
background-color: @msg-bg-color-light;
background-color: var(--msg-bg-color-light);
font-size: 14px;
padding: 10px;
position: relative;
@ -352,7 +376,7 @@
display: none;
font-size: 0.8em;
align-items: center;
color: @color;
color: @msg-color;
font-weight: bold;
position: absolute;
right: 0;
@ -372,7 +396,8 @@
}
}
.cp-app-contacts-input {
background-color: lighten(@bg-color, 15%);
background-color: @msg-bg-color-lighter;
background-color: var(--msg-bg-color-lighter);
height: auto;
min-height: 50px;
display: flex;
@ -385,22 +410,28 @@
border: none;
height: 54px; // 2 lines (22px height) + 2 margins (5px)
flex: 1;
background-color: darken(@bg-color, 10%);
color: @color;
background-color: white;
color: @cryptpad_text_col;
//background-color: @msg-bg-color-dark;
//background-color: var(--msg-bg-color-dark);
//color: @msg-color;
resize: none;
overflow-y: auto;
.placeholder(#bbb);
&[disabled="true"] {
.placeholder(#999);
&[disabled="true"] {
.placeholder(#666);
}
}
button {
height: 54px !important;
border-radius: 0;
border: none;
background-color: darken(@bg-color, 15%) !important;
color: #eee;
background-color: @msg-bg-color-darker;
background-color: var(--msg-bg-color-darker);
&:hover {
background-color: darken(@bg-color, 20%) !important;
background-color: @msg-bg-color-dark;
background-color: var(--msg-bg-color-dark);
}
}
}

View File

@ -1,8 +1,5 @@
@import (reference) "./colortheme-all.less";
@import (reference) "./variables.less";
@import (reference) './buttons.less';
.modal_base() {
font-family: @colortheme_font;
@ -39,8 +36,6 @@
background-color: @colortheme_modal-dim;
.cp-modal {
.buttons_main();
background-color: @colortheme_modal-bg;
color: @colortheme_modal-fg;
box-shadow: @variables_shadow;

View File

@ -26,6 +26,11 @@
// Properties modal
.cp-app-prop {
margin-bottom: 10px;
.cp-app-prop-hint {
color: @cryptpad_text_col;
font-size: 0.8em;
margin-bottom: 5px;
}
.cp-app-prop-size-container {
height: 20px;
background-color: @colortheme_logo-2;
@ -145,6 +150,16 @@
overflow: unset;
margin-bottom: 0;
}
pre.markmap {
margin-bottom: 0;
max-height: 100%;
overflow: hidden;
display: flex;
flex-flow: column;
svg {
flex: 1;
}
}
.cp-spinner {
border-color: @colortheme_logo-1;
border-top-color: transparent;

View File

@ -1,6 +1,5 @@
@import (reference) "/customize/src/less2/include/colortheme-all.less";
@import (reference) "/customize/src/less2/include/leftside-menu.less";
@import (reference) "/customize/src/less2/include/buttons.less";
@import (reference) "/customize/src/less2/include/browser.less";
@sidebar_button-width: 400px;
@ -98,7 +97,6 @@
}
}
margin-bottom: 20px;
.buttons_main();
}
[type="text"], [type="password"], button {
vertical-align: middle;

View File

@ -4,6 +4,9 @@
@msg-bg: #eee;
@fromme-bg: #ddd;
.cp-support-form-container {
div {
margin-bottom: 10px;
}
[type="text"] {
width: @sidebar_button-width;
margin-bottom: 10px;
@ -15,6 +18,18 @@
height: 300px;
}
}
.cp-support-attachments {
display: flex;
.fa {
cursor: pointer;
margin-right: 10px;
}
&> span {
border: 1px solid #ddd;
margin-right: 5px;
padding: 10px;
}
}
.cp-support-container {
.cp-support-list-ticket {
display: flex;
@ -52,6 +67,7 @@
}
}
.cp-support-list-actions {
display: flex;
order: 3;
.cp-support-hide {
display: none;

View File

@ -84,7 +84,6 @@
width: 100%;
min-width: 100% !important;
&:focus {
border-color: transparent;
outline: 0;
box-shadow: none;
}

View File

@ -82,7 +82,7 @@
}
}
button {
color: inherit;
color: @cryptpad_text_col;
background-color: rgba(0,0,0,0.2);
&:hover {
background-color: rgba(0,0,0,0.4);

View File

@ -16,21 +16,9 @@
@bg-color: @colortheme_default-bg, // color of the toolbar background
@warn-color: @colortheme_default-warn // color of the warning text in the toolbar
) {
@toolbar-color: @color;
@toolbar-color-l20: lighten(@color, 20%);
@toolbar-color-d20: darken(@color, 20%);
@toolbar-color-d15: darken(@color, 15%);
@toolbar-bg-color: @bg-color;
@toolbar-bg-color-l8: lighten(@bg-color, 8%);
@toolbar-bg-color-l20: lighten(@bg-color, 20%);
@toolbar-bg-color-d5: darken(@bg-color, 5%);
@toolbar-bg-color-d10: darken(@bg-color, 10%);
@toolbar-bg-color-d15: darken(@bg-color, 15%);
@toolbar-warn-color: @warn-color;
@toolbar-userlist-name-edit: contrast(@toolbar-color, @toolbar-color-l20, @toolbar-color-d20);
@toolbar-bg-color-light: lighten(@bg-color, 30%);
@toolbar-bg-color-active: lighten(@bg-color, 20%);
};
.toolbar_main (
@ -42,31 +30,11 @@
--LessLoader_require: LessLoader_currentFile();
.toolbar_vars(@color, @bg-color, @warn-color);
--toolbar-color: @toolbar-color;
--toolbar-color-l20: @toolbar-color-l20;
--toolbar-color-d20: @toolbar-color-d20;
--toolbar-color-d15: @toolbar-color-d15;
--toolbar-bg-color: @toolbar-bg-color;
--toolbar-bg-color-l8: @toolbar-bg-color-l8;
--toolbar-bg-color-l20: @toolbar-bg-color-l20;
--toolbar-bg-color-d5: @toolbar-bg-color-d5;
--toolbar-bg-color-d10: @toolbar-bg-color-d10;
--toolbar-bg-color-d15: @toolbar-bg-color-d15;
--toolbar-bg-color-light: @toolbar-bg-color-light;
--toolbar-bg-color-active: @toolbar-bg-color-active;
--toolbar-warn-color: @toolbar-warn-color;
--toolbar-userlist-name-edit: @toolbar-userlist-name-edit;
@media screen and (max-width: @barWidth) {
.cp-toolbar-rightside {
flex-wrap: wrap;
height: auto;
width: 100%;
}
}
.help_main(@color, @bg-color);
.help_main();
.notifications_main();
.dropdown_main();
.history_main();
@ -74,21 +42,13 @@
.modal_main();
};
& {
@keyframes notification {
0% {
background: rgba(0,0,0,0);
}
50% {
background: rgba(0,0,0,0.2);
}
100% {
background: rgba(0,0,0,0);
}
}
@toolbar-color: @cryptpad_text_col;
@toolbar-color-light: lighten(@cryptpad_text_col, 10%);
.toolbar_vars();
@toolbar-top-bg: #eee;
@toolbar_line-height: 32px;
@toolbar_top-height: 64px;
@toolbar_top-height: 76px;
@toolbar_button-font: @colortheme_app-font;
// if we spell 'share' correctly, then adblock plus hides the share button
@ -105,19 +65,16 @@
.cp-markdown-toolbar {
height: @toolbar_line-height;
background-color: @toolbar-bg-color-l20;
background-color: var(--toolbar-bg-color-l20);
background-color: @colortheme_pad-toolbar-bg;
display: none;
button {
height: @toolbar_line-height !important;
outline: 0;
color: @toolbar-color;
color: var(--toolbar-color);
.toolbar_button;
font: normal normal normal 14px/1 FontAwesome;
&:hover {
background-color: @toolbar-bg-color-l8;
background-color: var(--toolbar-bg-color-l8);
background-color: darken(@colortheme_pad-toolbar-bg, 5%);
}
&.cp-markdown-help { float: right; }
}
@ -166,15 +123,13 @@
padding: 0;
box-sizing: border-box;
position: relative;
order: -2;
resize: horizontal;
order: 99;
z-index: 1;
#cp-app-contacts-container {
height: 100%;
}
.cp-toolbar-chat-drawer-close {
color: @toolbar-color;
color: var(--toolbar-color);
position: absolute;
top: 0;
right: 1px;
@ -196,7 +151,7 @@
overflow-x: hidden;
padding: 10px;
box-sizing: border-box;
order: -3;
order: 100;
z-index: 1;
.cp-toolbar-userlist-drawer-close {
position: absolute;
@ -386,39 +341,21 @@
}
.cp-toolbar-userlist-drawer {
background-color: @toolbar-bg-color;
background-color: var(--toolbar-bg-color);
background-color: @colortheme_userlist-bg;
color: @toolbar-color;
color: var(--toolbar-color);
.cp-toolbar-userlist-drawer-close {
color: @toolbar-color;
color: var(--toolbar-color);
}
h2 {
background-color: @toolbar-bg-color-d10;
background-color: var(--toolbar-bg-color-d10);
color: @toolbar-color;
color: var(--toolbar-color);
}
.cp-toolbar-userlist-name-input {
background-color: @toolbar-bg-color-d10;
background-color: var(--toolbar-bg-color-d10);
color: @toolbar-color;
color: var(--toolbar-color);
background-color: rgba(0,0,0,0.1);
}
.cp-toolbar-userlist-button {
color: @toolbar-userlist-name-edit;
color: var(--toolbar-userlist-name-edit);
background: transparent;
&:hover {
color: @toolbar-color;
color: var(--toolbar-color);
}
}
.cp-toolbar-userlist-friend {
&:hover {
color: @toolbar-color-d15;
color: var(--toolbar-color-d15);
color: @toolbar-color-light;
}
}
}
@ -438,10 +375,10 @@
display: flex;
flex-wrap: wrap;
justify-content: space-between;
background-color: @toolbar-bg-color;
background-color: var(--toolbar-bg-color);
color: @toolbar-color;
color: var(--toolbar-color);
background-color: @toolbar-top-bg;
color: @toolbar-bg-color;
color: var(--toolbar-bg-color);
.fa {
font: normal normal normal 14px/1 FontAwesome;
@ -531,17 +468,13 @@
}
.cp-toolbar-title {
height: @toolbar_line-height;
flex-flow: row;
line-height: initial;
margin: 0;
.cp-toolbar-title-hoverable {
width: 100%;
}
.cp-toolbar-title-value-page {
padding: 5px;
line-height: unset;
border: 0;
}
.cp-toolbar-title-editable, .cp-toolbar-title-value-page {
.cp-toolbar-title-editable, .cp-toolbar-title-value {
max-width: ~"calc(100vw - 26px)";
display: inline-block;
overflow: hidden;
@ -549,13 +482,21 @@
font-size: @colortheme_app-font-size;
height: @toolbar_line-height;
box-sizing: border-box;
line-height: 20px;
line-height: @toolbar_line-height;
}
.cp-toolbar-title-readonly {
font-size: 14px;
}
.cp-toolbar-title-value {
padding: 5px;
line-height: @toolbar_line-height - 10px;
border: 0;
}
.cp-toolbar-title-edit, .cp-toolbar-title-save {
box-sizing: border-box;
height: @toolbar_line-height;
line-height: @colortheme_app-font-size;
display: inline-block;
line-height: @toolbar_line-height;
display: inline-flex;
.fa {
font-size: @colortheme_app-font-size;
@ -569,9 +510,16 @@
line-height: calc(@toolbar_line-height - 12px); // padding + border
}
}
.cp-toolbar-spinner {
min-width: 100px;
margin: 0;
height: @toolbar_line-height;
line-height: @toolbar_line-height;
}
.cp-toolbar-link {
height: @toolbar_line-height;
width: @toolbar_line-height;
transform: scale(0.5);
.cp-toolbar-link-logo {
padding: 5px;
}
@ -594,17 +542,6 @@
}
}
}
.cp-toolbar-new {
height: @toolbar_line-height;
width: @toolbar_line-height;
margin-left: 0;
button {
height: @toolbar_line-height;
width: @toolbar_line-height;
font-size: 20px;
margin-top: -1px;
}
}
.cp-toolbar-user-dropdown {
height: @toolbar_line-height;
width: @toolbar_line-height;
@ -628,105 +565,11 @@
margin: 0;
}
}
/*
.cp-toolbar-top-filler {
flex: 1;
}
.cp-toolbar-title {
flex: auto;
width: 100%;
order: 10;
height: @toolbar_line-height;
line-height: initial;
margin: 0;
.cp-toolbar-title-hoverable {
width: 100%;
}
.cp-toolbar-title-editable {
max-width: ~"calc(100vw - 26px)";
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
font-size: @colortheme_app-font-size;
height: @toolbar_line-height;
box-sizing: border-box;
line-height: 20px;
}
.cp-toolbar-title-edit, .cp-toolbar-title-save {
box-sizing: border-box;
height: @toolbar_line-height;
line-height: @colortheme_app-font-size;
display: inline-block;
.fa {
font-size: @colortheme_app-font-size;
}
}
input {
height: @toolbar_line-height;
font-size: @colortheme_app-font-size;
flex: 1;
max-width: none;
line-height: calc(@toolbar_line-height - 12px); // padding + border
}
}
*/
}
}
.cp-toolbar-spinner {
font-size: @colortheme_app-font-size;
color: @toolbar-color;
color: var(--toolbar-color);
}
.cp-toolbar-limit {
text-shadow: -1px 0 @toolbar-color, 0 1px @toolbar-color, 1px 0 @toolbar-color, 0 -1px @toolbar-color;
text-shadow: -1px 0 var(--toolbar-color), 0 1px var(--toolbar-color), 1px 0 var(--toolbar-color), 0 -1px var(--toolbar-color);
color: @toolbar-warn-color;
color: var(--toolbar-warn-color);
}
.cp-toolbar-leftside, .cp-toolbar-rightside {
background-color: @toolbar-bg-color-l8;
background-color: var(--toolbar-bg-color-l8);
button:hover, button.cp-toolbar-button-active {
background-color: @toolbar-bg-color;
background-color: var(--toolbar-bg-color);
}
}
.cp-toolbar-title-hoverable:hover {
.cp-toolbar-title-editable, .cp-toolbar-title-edit {
cursor: text;
border: 1px solid @toolbar-bg-color-d15;
border: 1px solid var(--toolbar-bg-color-d15);
background: @toolbar-bg-color-d10;
background: var(--toolbar-bg-color-d10);
transition: all 0.15s;
color: @toolbar-color;
color: var(--toolbar-color);
}
.cp-toolbar-title-editable {
cursor: text;
}
}
.cp-toolbar-title-save {
border: 1px solid @toolbar-bg-color-d15;
border: 1px solid var(--toolbar-bg-color-d15);
background: @toolbar-bg-color-d10;
background: var(--toolbar-bg-color-d10);
color: @toolbar-color;
color: var(--toolbar-color);
&:hover {
background: @toolbar-bg-color-d5;
background: var(--toolbar-bg-color-d5);
}
}
input {
border: 1px solid @toolbar-bg-color-d15;
border: 1px solid var(--toolbar-bg-color-d15);
background: @toolbar-bg-color-d10;
background: var(--toolbar-bg-color-d10);
color: @toolbar-color;
color: var(--toolbar-color);
color: @colortheme_toolbar-warn;
}
.cp-dropdown-content.cp-dropdown-left a {
color: black;
@ -752,8 +595,7 @@
padding: 0;
margin: 0 5px;
font-size: @colortheme_app-font-size;
color: @toolbar-warn-color;
color: var(--toolbar-warn-color);
color: @colortheme_toolbar-warn;
.cp-pnp-msg {
padding-left: 5px;
font-family: @colortheme_font;
@ -762,8 +604,7 @@
font-size: @colortheme_app-font-size;
font-family: @colortheme_font;
font-weight: bold;
color: @toolbar-warn-color;
color: var(--toolbar-warn-color);
color: @colortheme_toolbar-warn;
&:hover {
text-decoration: underline;
}
@ -784,34 +625,32 @@
//flex: 1;
}
.cp-toolbar-title {
color: @cryptpad_text_col;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
order: 3;
height: 100%;
display: inline-flex;
align-items: center;
line-height: @toolbar_top-height;
flex-flow: column;
justify-content: center;
margin: 0 10px;
.cp-toolbar-title-value {
border: 1px solid transparent;
padding: 5px;
font-size: 25px;
padding: 0 5px;
font-size: 30px;
vertical-align: middle;
line-height: 25px;
white-space: nowrap;
}
.cp-toolbar-title-value-page {
border: 1px solid transparent;
padding: 0 5px;
line-height: 48px;
}
.cp-toolbar-title-edit, .cp-toolbar-title-save {
display: flex;
align-items: center;
font-size: 20px;
vertical-align: middle;
line-height: 20px;
.fa {
font-size: 20px;
}
@ -821,21 +660,37 @@
font-size: 25px;
font-style: italic;
white-space: nowrap;
display: inline-flex;
align-items: center;
}
.cp-toolbar-title-hoverable {
display: inline-flex;
overflow: hidden;
align-self: baseline;
max-width: 100%;
&:hover {
.cp-toolbar-title-editable, .cp-toolbar-title-edit {
border: 1px solid @toolbar-bg-color;
border: 1px solid var(--toolbar-bg-color);
transition: all 0.15s;
}
.cp-toolbar-title-editable {
cursor: text;
}
}
}
.cp-toolbar-title-edit {
cursor: pointer;
border: 1px solid transparent;
padding: 5px;
padding: 0 5px;
border-collapse: collapse;
span {
cursor: pointer;
}
}
.cp-toolbar-title-save {
border: 1px solid @toolbar-bg-color;
border: 1px solid var(--toolbar-bg-color);
cursor: pointer;
padding: 5px;
border-collapse: collapse;
@ -843,54 +698,45 @@
cursor: pointer;
}
}
.cp-toolbar-title-editable {
.cp-toolbar-title-value {
overflow: hidden;
text-overflow: ellipsis;
border-collapse: collapse;
}
input {
color: @cryptpad_text_col;
border: 1px solid @toolbar-bg-color;
border: 1px solid var(--toolbar-bg-color);
max-width: ~"calc(100% - 40px)";
//flex: 1;
vertical-align: middle;
box-sizing: border-box;
cursor: auto;
width: 300px;
font-size: 20px;
padding: 5px 5px;
height: 40px;
line-height: 28px; // padding + border
font-size: 30px !important;
padding: 0 5px !important;
height: 43px;
}
}
.cp-toolbar-link, .cp-toolbar-new, .cp-toolbar-notifications {
font-size: 48px;
line-height: 64px;
.cp-toolbar-link, .cp-toolbar-notifications {
line-height: @toolbar_top-height;
width: @toolbar_top-height;
height: @toolbar_top-height;
padding: 0;
box-sizing: border-box;
display: inline-block;
color: white;
a {
color: white;
}
transition: all 0.15s;
}
.cp-toolbar-notifications, .cp-toolbar-new {
background-color: rgba(0,0,0,0.2);
&:hover {
background-color: rgba(0,0,0,0.3);
}
.cp-toolbar-notifications {
text-align: center;
font-size: 32px;
margin-left: 10px;
&> button {
display: flex;
align-items: center;
justify-content: center;
width: 64px;
width: @toolbar_top-height;
font-size: 1em;
color: inherit;
height: 64px;
height: @toolbar_top-height;
padding: 0px;
margin: 0;
&::before {
@ -899,8 +745,8 @@
padding-top: 4px;
}
&:hover {
background-color: initial;
border-color: transparent;
background-color: rgba(50,50,50,0.1);
}
span {
vertical-align: top;
@ -909,9 +755,6 @@
color: inherit;
}
}
}
.cp-toolbar-notifications {
margin-left: 10px;
.cp-notifications-empty {
color: black;
padding: 5px;
@ -920,14 +763,15 @@
position: relative;
.cp-dropdown-button-title {
position: absolute;
bottom: 0;
right: 0;
bottom: 10px;
right: 16px;
font-size: 14px;
border: 1px solid;
border-radius: 50%;
width: 20px;
height: 20px;
line-height: 16px;
background: @toolbar-top-bg;
&.cp-notifications-small {
font-size: 10px;
line-height: 17px;
@ -940,10 +784,6 @@
align-items: center;
justify-content: center;
cursor: pointer;
background-color: rgba(0,0,0,0.4);
&:hover {
background-color: rgba(0,0,0,0.5);
}
order: 1;
.fa {
margin: 0;
@ -955,6 +795,13 @@
height: auto;
padding: 10px;
svg {
* {
fill: @toolbar-bg-color;
fill: var(--toolbar-bg-color);
}
}
img {
cursor: pointer;
height: 100%;
@ -967,9 +814,7 @@
display: inline-flex;
order: 6;
line-height: @toolbar_top-height;
color: white;
.cp-toolbar-notifications { order: 1; }
.cp-toolbar-new { order: 2; }
.cp-toolbar-user-dropdown { order: 3; }
.cp-toolbar-backup { order: 4; } // TODO drive migration to secure iframe
&> * {
@ -980,40 +825,44 @@
.cp-toolbar-user-dropdown {
z-index: 10000; //Z cp-toolbar-user-dropdown
//margin-left: 20px;
height: 64px;
width: 64px;
height: @toolbar_top-height;
width: @toolbar_top-height;
padding: 0px;
box-sizing: border-box;
text-align: center;
/*
background-color: rgba(0,0,0,0.3);
transition: all 0.15s;
&:hover {
background-color: rgba(0,0,0,0.4);
}
*/
.cp-dropdown-content {
margin: 0;
overflow: visible;
}
& > button {
display: flex;
justify-content: center;
align-items: center;
height: 64px;
width: 64px;
height: @toolbar_top-height;
width: @toolbar_top-height;
padding: 0;
&:hover {
border-color: transparent;
background-color: rgba(50,50,50,0.1);
}
span {
text-align: center;
width: 100%;
font-size: 32px;
font-size: 48px;
display: inline-flex;
justify-content: center;
align-items: center;
}
&.cp-avatar {
.avatar_main(48px);
media-tag {
margin: 8px;
}
.avatar_main(64px);
border: 0;
}
}
@ -1037,16 +886,37 @@
}
}
.cp-toolbar-leftside {
//height: @toolbar_line-height;
.cp-toolbar-history {
background-color: @toolbar-bg-color-light;
background-color: var(--toolbar-bg-color-light);
color: @cryptpad_text_col;
}
.cp-toolbar-bottom {
background-color: @toolbar-bg-color-light;
background-color: var(--toolbar-bg-color-light);
color: @cryptpad_text_col;
button:hover, button.cp-toolbar-button-active {
background-color: @toolbar-bg-color-active;
background-color: var(--toolbar-bg-color-active);
}
button.cp-toolbar-button-active {
border-color: @toolbar-bg-color-active;
border-color: var(--toolbar-bg-color-active);
}
button:hover, button:focus {
border-color: @toolbar-bg-color-active;
border-color: var(--toolbar-bg-color-active);
//border-color: @cryptpad_text_col;
}
display: inline-flex;
align-items: center;
justify-content: space-between;
max-width: 100%;
flex: 1 1 auto;
&:empty {
height: 0;
}
display: inline-flex;
align-items: center;
max-width: 100%;
flex: 1 1 auto;
//margin-bottom: -1px;
.cp-toolbar-users {
pre {
/* needed for ckeditor */
@ -1055,62 +925,92 @@
}
}
button {
border: 1px solid transparent;
box-sizing: border-box;
position: relative;
margin: 0px;
border-radius: 0;
height: 100%;
height: @toolbar_line-height;
display: inline-flex;
align-items: center;
.fa, .cptools {
margin-right: 5px;
}
}
&.cp-toolbar-small {
button {
.cp-toolbar-name, .cp-button-name {
display: none;
}
i {
margin-right: 0;
}
}
.cp-dropdown-content {
margin-top: -1px;
}
& > span {
height: @toolbar_line-height;
}
#cp-toolbar-userlist-drawer-open { order: 0; }
#cp-toolbar-chat-drawer-open { order: 1; }
.cp-toolbar-share-button { order: 2; }
.cp-toolbar-spinner { order: 3; }
#cp-toolbar-userlist-drawer-open button {
width: 125px;
text-align: center;
}
#cp-toolbar-chat-drawer-open button {
&.cp-toolbar-notification {
animation: notification 2s ease-in-out infinite;
}
}
.cp-toolbar-share-button {
width: 50px;
text-align: center;
}
}
.cp-toolbar-rightside {
.cp-toolbar-bottom-left {
display: flex;
min-height: @toolbar_line-height;
overflow: hidden;
&:empty {
min-height: 0;
height: 0;
order: 1;
}
.cp-toolbar-bottom-mid {
display: flex;
order: 2;
}
.cp-toolbar-bottom-right {
order: 3;
display: flex;
#cp-toolbar-chat-drawer-open { order: 0; }
#cp-toolbar-userlist-drawer-open { order: 1; }
}
.cp-toolbar-rightside-button {
cursor: pointer;
// UI actions
&.cp-toolbar-icon-toggle { order: 1; }
&.cp-toolbar-icon-preview { order: 2; }
&.cp-toolbar-icon-present { order: 3; }
// Content actions
&.cp-toolbar-icon-mediatag { order: 10; }
order: 11;
// Storage actions
&.cp-toolbar-icon-hashtag { order: 20; }
&.cp-toolbar-icon-template { order: 21; }
&.cp-toolbar-icon-forget { order: 22; }
// Drawer
&.cp-toolbar-drawer-button { order: 30; }
.cp-toolbar-bottom-right {
button {
white-space: nowrap;
}
}
.cp-toolbar-bottom-left {
.cp-toolbar-appmenu, .cp-toolbar-file {
button {
&::before, .fa {
min-width: 20px;
text-align: center;
margin-right: 0;
}
}
}
.cp-toolbar-file {
order: 1;
}
.cp-toolbar-appmenu {
order: 3;
}
.cp-toolbar-mediatag, .cp-toolbar-icon-mediatag {
order: 5;
}
.cp-toolbar-tools {
order: 7;
}
.cp-toolbar-file {
button {
&.fa-plus { order: 0; }
&.fa-history { order: 5; }
&.fa-hashtag { order: 10; }
&.fa-bookmark { order: 15; }
&.fa-upload { order: 20; }
&.fa-clone { order: 25; }
&.fa-download { order: 30; }
&.fa-print { order: 35; }
&.fa-trash { order: 40; }
&.fa-info-circle { order: 100; }
&.cp-toolbar-icon-help { order: 150; }
}
}
.cp-toolbar-drawer-content:empty ~ .cp-toolbar-drawer-button {
@ -1118,15 +1018,22 @@
}
.cp-toolbar-drawer-content {
box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.2);
overflow-y: auto;
overflow-x: hidden;
&.cp-dropdown-visible {
overflow: visible;
}
position: absolute;
right: 0px;
margin-top: @toolbar_line-height;
left: 0px;
top: @toolbar_line-height;
margin: -1px;
min-width: 50px;
background: @colortheme_dropdown-bg;
display: flex;
flex-flow: column;
z-index: 10000; //Z cp-toolbar-drawer-content
color: black;
.tools_unselectable();
.fa {
font-size: 17px;
}
@ -1139,6 +1046,7 @@
border: 0;
}
button {
white-space: nowrap;
padding: 5px 16px;
text-align: left;
margin: 0;
@ -1154,45 +1062,27 @@
display: inline;
vertical-align: baseline;
}
&.fa-info-circle, &.fa-history, &.fa-cog {
.cp-toolbar-drawer-element {
margin-left: 11px;
}
}
&.fa-unlock-alt {
.cp-toolbar-drawer-element {
margin-left: 15px;
}
}
&.fa-question {
.cp-toolbar-drawer-element {
margin-left: 16px;
}
}
&:hover {
background-color: @colortheme_dropdown-bg-hover !important;
color: @colortheme_dropdown-color;
}
order: 8;
&.fa-history { order: 1; }
&.fa-clone { order: 1; }
&.fa-download { order: 2; }
&.fa-upload { order: 3; }
&.fa-print { order: 4; }
&.fa-cog { order: 5; }
&.fa-info-circle { order: 6; }
&.fa-help { order: 7; }
}
}
}
}
.cp-toolbar-spinner {
line-height: @toolbar_line-height;
padding: 0 20px;
color: @cryptpad_text_col;
font-size: 14px;
padding: 0 8px;
height: 20px;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 200px;
box-sizing: border-box;
margin-bottom: 5px;
&> span.fa {
height: 20px;
width: 20px;

View File

@ -2,13 +2,9 @@
&::-webkit-input-placeholder { /* WebKit, Blink, Edge */
color: @color;;
}
&:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
&::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: @color;
opacity: 1;
}
&::-moz-placeholder { /* Mozilla Firefox 19+ */
color: @color;
opacity: 1;
opacity: 1; /* Firefox */
}
&:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: @color;

View File

@ -2,9 +2,11 @@
@import (reference) "../include/colortheme-all.less";
@import (reference) "../include/alertify.less";
@import (reference) "../include/checkmark.less";
@import (reference) "../include/forms.less";
&.cp-page-register {
.infopages_main();
.forms_main();
.alertify_main();
.checkmark_main(20px);

View File

@ -2,7 +2,7 @@ html, body {
margin: 0px;
padding: 0px;
}
#sbox-iframe, #sbox-share-iframe, #sbox-filePicker-iframe {
#sbox-iframe, #sbox-secure-iframe {
position: fixed;
top:0; left:0;
bottom:0; right:0;

View File

@ -1,31 +0,0 @@
version: '2'
services:
cryptpad:
build:
context: .
args:
- VERSION=${VERSION}
image: "xwiki/cryptpad:${VERSION}"
hostname: cryptpad
labels:
- traefik.port=3000
- traefik.frontend.passHostHeader=true
environment:
- USE_SSL=${USE_SSL}
- STORAGE=${STORAGE}
- LOG_TO_STDOUT=${LOG_TO_STDOUT}
ports:
- "3000:3000"
- "3001:3001"
restart: always
volumes:
- ./data/files:/cryptpad/datastore:rw
- ./data/customize:/cryptpad/customize:rw
- ./data/blob:/cryptpad/blob:rw
- ./data/block:/cryptpad/block:rw
- ./data/config:/cryptpad/cfg:rw
- ./data/data:/cryptpad/data:rw

View File

@ -1,25 +0,0 @@
#!/bin/sh
# Figure out latest release via GitHub API
release=$(curl --silent "https://api.github.com/repos/krallin/tini/releases/latest" | jq -r .tag_name)
# _Reliable_ way to get which arch for tini download
arch=$(python <<EOF
from __future__ import print_function
import platform
processor = platform.machine()
if processor == 'aarch64':
print('arm64', end='')
elif processor == 'x86 64' or processor == 'x86_64':
print('amd64', end='')
elif processor == 'armv7l':
print('armhf', end='')
EOF
)
# Download/install tini
curl -L https://github.com/krallin/tini/releases/download/$release/tini-static-$arch \
-o /sbin/tini
chmod a+x /sbin/tini

View File

@ -1,79 +0,0 @@
# Cryptpad Docker Image
Cryptpad includes support for building a Docker image and running it to provide a Cryptpad instance. You can manage the container manually, or let Docker Compose manage it for you.
A full tutorial is available [on the Cryptpad Github wiki](https://github.com/xwiki-labs/cryptpad/wiki/Docker). This document provides a brief overview.
## Features
- Configuration via .env file
- Ready for use with traffic
- Using github master for now, release 0.3.0 too old
- Creating customize folder
- Adding config.js to customize folder
- Persistance for datastore and customize folder
## Run
Run from the cryptpad source directory, keeping instance state in `/var/cryptpad`:
```
docker build -t xwiki/cryptpad .
docker run --restart=always -d --name cryptpad -p 3000:3000 -p 3001:3001 \
-v /var/cryptpad/files:/cryptpad/datastore \
-v /var/cryptpad/customize:/cryptpad/customize \
-v /var/cryptpad/blob:/cryptpad/blob \
-v /var/cryptpad/blobstage:/cryptpad/blobstage \
-v /var/cryptpad/pins:/cryptpad/pins \
-v /var/cryptpad/tasks:/cryptpad/tasks \
-v /var/cryptpad/block:/cryptpad/block \
xwiki/cryptpad
```
Or, using docker-compose and the included `docker-compose.yml`, keeping instance state in the current directory under `./data`:
```
docker-compose up -d
```
## Configuration
Set configurations Dockerfile or in .env (using docker-compose) file.
- VERSION=latest
- USE_SSL=false
- STORAGE='./storage/file'
- LOG_TO_STDOUT=true
The .env variables are read by docker-compose and forwarded to docker container.
On runtime, in `bin/container-start.sh` the settings are written to the `config.js` file.
## Persistance
The docker-compose file is preconfigured to persist folders
- cryptpad/datastore --> ./data/files
- cryptpad/customize --> ./data/customize
- cryptpad/pins --> ./data/pins
- cryptpad/blob --> ./data/blob
- cryptpad/blobstage --> ./data/blobstage
- cryptpad/tasks --> ./data/tasks
- cryptpad/block --> ./data/block
Your configuration file will be in `./data/customize/config.js`.
The data folder is ignored by git, so if you want to add your customizations to git versioning change the volume:
```
./customize:/cryptpad/customize:rw
```
## SSL Proxy
The [traefik](https://traefik.io/) proxy has builtin Let'sEncrypt for easy SSL setup.
In the docker-compose file you can find preset lables for usage with traefik.
[Traefik Docker Image](https://hub.docker.com/_/traefik/)
Alternativly just use plain old nginx.

View File

@ -54,6 +54,7 @@ server {
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 "*";
# add_header X-Frame-Options "SAMEORIGIN";
# Insert the path to your CryptPad repository root here
@ -101,7 +102,6 @@ server {
set $unsafe 0;
# the following assets are loaded via the sandbox domain
# they unfortunately still require exceptions to the sandboxing to work correctly.
if ($uri = "/pad/inner.html") { set $unsafe 1; }
if ($uri = "/sheet/inner.html") { set $unsafe 1; }
if ($uri ~ ^\/common\/onlyoffice\/.*\/index\.html.*$) { set $unsafe 1; }

View File

@ -9,6 +9,17 @@ module.exports.create = function (config) {
var log = config.log;
var noop = function () {};
var special_errors = {};
['EPIPE', 'ECONNRESET'].forEach(function (k) { special_errors[k] = noop; });
special_errors.NF_ENOENT = function (error, label, info) {
delete info.stack;
log.error(label, {
info: info,
});
};
// spawn ws server and attach netflux event handlers
NetfluxSrv.create(new WebSocketServer({ server: config.httpServer}))
.on('channelClose', historyKeeper.channelClose)
@ -17,11 +28,18 @@ module.exports.create = function (config) {
.on('sessionClose', historyKeeper.sessionClose)
.on('error', function (error, label, info) {
if (!error) { return; }
if (['EPIPE', 'ECONNRESET'].indexOf(error && error.code) !== -1) { return; }
var code = error && (error.code || error.message);
if (code) {
/* EPIPE,ECONNERESET, NF_ENOENT */
if (typeof(special_errors[code]) === 'function') {
return void special_errors[code](error, label, info);
}
}
/* labels:
SEND_MESSAGE_FAIL, SEND_MESSAGE_FAIL_2, FAIL_TO_DISCONNECT,
FAIL_TO_TERMINATE, HANDLE_CHANNEL_LEAVE, NETFLUX_BAD_MESSAGE,
NETFLUX_WEBSOCKET_ERROR, NF_ENOENT
NETFLUX_WEBSOCKET_ERROR
*/
log.error(label, {
code: error.code,

View File

@ -1,4 +1,5 @@
/*jshint esversion: 6 */
/* globals process */
const nThen = require("nthen");
const getFolderSize = require("get-folder-size");
const Util = require("../common-util");
@ -50,6 +51,7 @@ var getCacheStats = function (env, server, cb) {
metaSize: metaSize,
channel: channelCount,
channelSize: channelSize,
memoryUsage: process.memoryUsage(),
});
};

View File

@ -54,22 +54,29 @@ Channel.clearOwnedChannel = function (Env, safeKey, channelId, cb, Server) {
});
};
Channel.removeOwnedChannel = function (Env, safeKey, channelId, cb, Server) {
if (typeof(channelId) !== 'string' || !Core.isValidId(channelId)) {
return cb('INVALID_ARGUMENTS');
}
var archiveOwnedChannel = function (Env, safeKey, channelId, _cb, Server) {
var unsafeKey = Util.unescapeKeyCharacters(safeKey);
if (Env.blobStore.isFileId(channelId)) {
return void Env.removeOwnedBlob(channelId, safeKey, cb);
nThen(function (w) {
// confirm that the channel exists before worrying about whether
// we have permission to delete it.
var cb = _cb;
Env.msgStore.getChannelSize(channelId, w(function (err, bytes) {
if (!bytes) {
w.abort();
return cb(err || "ENOENT");
}
}));
}).nThen(function (w) {
var cb = Util.both(w.abort, _cb);
Metadata.getMetadata(Env, channelId, function (err, metadata) {
if (err) { return void cb(err); }
if (!Core.hasOwners(metadata)) { return void cb('E_NO_OWNERS'); }
if (!Core.isOwner(metadata, unsafeKey)) {
return void cb('INSUFFICIENT_PERMISSIONS');
}
});
}).nThen(function () {
var cb = _cb;
// temporarily archive the file
return void Env.msgStore.archiveChannel(channelId, function (e) {
Env.Log.info('ARCHIVAL_CHANNEL_BY_OWNER_RPC', {
@ -124,6 +131,24 @@ Channel.removeOwnedChannel = function (Env, safeKey, channelId, cb, Server) {
});
};
Channel.removeOwnedChannel = function (Env, safeKey, channelId, __cb, Server) {
var _cb = Util.once(Util.mkAsync(__cb));
if (typeof(channelId) !== 'string' || !Core.isValidId(channelId)) {
return _cb('INVALID_ARGUMENTS');
}
// archiving large channels or files can be expensive, so do it one at a time
// for any given user to ensure that nobody can use too much of the server's resources
Env.queueDeletes(safeKey, function (next) {
var cb = Util.both(_cb, next);
if (Env.blobStore.isFileId(channelId)) {
return void Env.removeOwnedBlob(channelId, safeKey, cb);
}
archiveOwnedChannel(Env, safeKey, channelId, cb, Server);
});
};
Channel.trimHistory = function (Env, safeKey, data, cb) {
if (!(data && typeof(data.channel) === 'string' && typeof(data.hash) === 'string' && data.hash.length === 64)) {
return void cb('INVALID_ARGS');

View File

@ -2,7 +2,8 @@
/* globals Buffer*/
const Quota = module.exports;
const Util = require("../common-util");
//const Util = require("../common-util");
const Keys = require("../keys");
const Package = require('../../package.json');
const Https = require("https");
@ -19,11 +20,18 @@ Quota.applyCustomLimits = function (Env) {
var customLimits = (function (custom) {
var limits = {};
Object.keys(custom).forEach(function (k) {
k.replace(/\/([^\/]+)$/, function (all, safeKey) {
var id = Util.unescapeKeyCharacters(safeKey || '');
limits[id] = custom[k];
return '';
var user;
try {
user = Keys.parseUser(k);
} catch (err) {
return void Env.Log.error("PARSE_CUSTOM_LIMIT_BLOCK", {
user: k,
error: err.message,
});
}
var unsafeKey = user.pubkey;
limits[unsafeKey] = custom[k];
});
return limits;
}(Env.customLimits || {}));

View File

@ -75,8 +75,21 @@ Upload.upload = function (Env, safeKey, chunk, cb) {
Env.blobStore.upload(safeKey, chunk, cb);
};
var reportStatus = function (Env, label, safeKey, err, id) {
var data = {
safeKey: safeKey,
err: err && err.message || err,
id: id,
};
var method = err? 'error': 'info';
Env.Log[method](label, data);
};
Upload.complete = function (Env, safeKey, arg, cb) {
Env.blobStore.complete(safeKey, arg, cb);
Env.blobStore.complete(safeKey, arg, function (err, id) {
reportStatus(Env, 'UPLOAD_COMPLETE', safeKey, err, id);
cb(err, id);
});
};
Upload.cancel = function (Env, safeKey, arg, cb) {
@ -84,6 +97,9 @@ Upload.cancel = function (Env, safeKey, arg, cb) {
};
Upload.complete_owned = function (Env, safeKey, arg, cb) {
Env.blobStore.completeOwned(safeKey, arg, cb);
Env.blobStore.completeOwned(safeKey, arg, function (err, id) {
reportStatus(Env, 'UPLOAD_COMPLETE_OWNED', safeKey, err, id);
cb(err, id);
});
};

View File

@ -31,13 +31,14 @@ module.exports.create = function (config, cb) {
// and more easily share state between historyKeeper and rpc
const Env = {
Log: Log,
// tasks
// store
id: Crypto.randomBytes(8).toString('hex'),
metadata_cache: {},
channel_cache: {},
queueStorage: WriteQueue(),
queueDeletes: WriteQueue(),
queueValidation: WriteQueue(),
batchIndexReads: BatchRead("HK_GET_INDEX"),
batchMetadata: BatchRead('GET_METADATA'),
@ -98,7 +99,7 @@ module.exports.create = function (config, cb) {
paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
paths.blob = keyOrDefaultString('blobPath', './blob');
Env.defaultStorageLimit = typeof(config.defaultStorageLimit) === 'number' && config.defaultStorageLimit > 0?
Env.defaultStorageLimit = typeof(config.defaultStorageLimit) === 'number' && config.defaultStorageLimit >= 0?
config.defaultStorageLimit:
Core.DEFAULT_LIMIT;
@ -252,17 +253,14 @@ module.exports.create = function (config, cb) {
channelExpirationMs: config.channelExpirationMs,
verbose: config.verbose,
openFileLimit: config.openFileLimit,
maxWorkers: config.maxWorkers,
}, w(function (err) {
if (err) {
throw new Error(err);
}
}));
}).nThen(function (w) {
// create a task store (for scheduling tasks)
require("./storage/tasks").create(config, w(function (e, tasks) {
if (e) { throw e; }
Env.tasks = tasks;
}));
}).nThen(function () {
if (config.disableIntegratedTasks) { return; }
config.intervals = config.intervals || {};

View File

@ -325,6 +325,9 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash) {
}
}));
}).nThen((waitFor) => {
/* TODO we can skip updating the index if there's nobody in the channel.
Populating it might actually be the cause of a memory leak.
*/
getIndex(Env, id, waitFor((err, index) => {
if (err) {
Log.warn("HK_STORE_MESSAGE_INDEX", err.stack);
@ -340,7 +343,12 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash) {
line: ((index.line || 0) + 1)
});
}
if (optionalMessageHash) {
/* This 'getIndex' call will construct a new index if one does not already exist.
If that is the case then our message will already be present and updating our offset map
can actually cause it to become incorrect, leading to incorrect behaviour when clients connect
with a lastKnownHash. We avoid this by only assigning new offsets to the map.
*/
if (optionalMessageHash && typeof(index.offsetByHash[optionalMessageHash]) === 'undefined') {
index.offsetByHash[optionalMessageHash] = index.size;
index.offsets++;
}
@ -529,7 +537,7 @@ const handleFirstMessage = function (Env, channelName, metadata) {
if(metadata.expire && typeof(metadata.expire) === 'number') {
// the fun part...
// the user has said they want this pad to expire at some point
Env.tasks.write(metadata.expire, "EXPIRE", [ channelName ], function (err) {
Env.writeTask(metadata.expire, "EXPIRE", [ channelName ], function (err) {
if (err) {
// if there is an error, we don't want to crash the whole server...
// just log it, and if there's a problem you'll be able to fix it
@ -621,7 +629,11 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) {
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(msg)], readMore);
}, (err) => {
if (err && err.code !== 'ENOENT') {
if (err.message !== 'EINVAL') { Log.error("HK_GET_HISTORY", err); }
if (err.message !== 'EINVAL') { Log.error("HK_GET_HISTORY", {
channel: channelName,
err: err && err.message || err,
stack: err && err.stack,
}); }
const parsedMsg = {error:err.message, channel: channelName, txid: txid};
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(parsedMsg)]);
return;
@ -662,30 +674,17 @@ const handleGetHistoryRange = function (Env, Server, seq, userId, parsed) {
}
Server.send(userId, [seq, 'ACK']);
Env.getOlderHistory(channelName, oldestKnownHash, function (err, messages) {
Env.getOlderHistory(channelName, oldestKnownHash, desiredMessages, desiredCheckpoint, function (err, toSend) {
if (err && err.code !== 'ENOENT') {
Env.Log.error("HK_GET_OLDER_HISTORY", err);
}
if (!Array.isArray(messages)) { messages = []; }
var toSend = [];
if (typeof (desiredMessages) === "number") {
toSend = messages.slice(-desiredMessages);
} else {
let cpCount = 0;
for (var i = messages.length - 1; i >= 0; i--) {
if (/^cp\|/.test(messages[i][4]) && i !== (messages.length - 1)) {
cpCount++;
}
toSend.unshift(messages[i]);
if (cpCount >= desiredCheckpoint) { break; }
}
}
if (Array.isArray(toSend)) {
toSend.forEach(function (msg) {
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId,
JSON.stringify(['HISTORY_RANGE', txid, msg])]);
});
}
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId,
JSON.stringify(['HISTORY_RANGE_END', txid, channelName])
@ -845,6 +844,9 @@ HK.onChannelMessage = function (Env, Server, channel, msgStruct) {
if (isCp) {
// id becomes either null or an array or results...
id = CHECKPOINT_PATTERN.exec(msgStruct[4]);
// FIXME relying on this data to be stored on an in-memory structure
// managed by a dependency is fragile. We should put this somewhere
// more straightforward and reliable.
if (Array.isArray(id) && id[2] && id[2] === channel.lastSavedCp) {
// Reject duplicate checkpoints
return;
@ -868,15 +870,21 @@ HK.onChannelMessage = function (Env, Server, channel, msgStruct) {
// trim the checkpoint indicator off the message if it's present
let signedMsg = (isCp) ? msgStruct[4].replace(CHECKPOINT_PATTERN, '') : msgStruct[4];
// convert the message from a base64 string into a Uint8Array
//const txid = Util.uid();
// Listen for messages
//console.log(+new Date(), "Send verification request");
Env.validateMessage(signedMsg, metadata.validateKey, w(function (err) {
/* queueing this helps avoid race conditions in which workers
validate and write messages in a different order than they were received.
For best effect the validate and store should actually be queued atomically,
but this is a step in the right direction.
*/
var proceed = w();
Env.queueValidation(channel.id, function (next) {
Env.validateMessage(signedMsg, metadata.validateKey, function (err) {
// always go on to the next item in the queue regardless of the outcome
next();
// no errors means success
if (!err) { return; }
if (!err) { return proceed(); }
// validation can fail in multiple ways
if (err === 'FAILED') {
// we log this case, but not others for some reason
@ -884,7 +892,8 @@ HK.onChannelMessage = function (Env, Server, channel, msgStruct) {
}
// always abort if there was an error...
return void w.abort();
}));
});
});
}).nThen(function () {
// do checkpoint stuff...
@ -903,6 +912,8 @@ HK.onChannelMessage = function (Env, Server, channel, msgStruct) {
// FIXME
if (Array.isArray(id) && id[2]) {
// Store new checkpoint hash
// there's a FIXME above which concerns a reference to `lastSavedCp`
// this is a hacky place to store important data.
channel.lastSavedCp = id[2];
}
}

1
lib/keys.js Normal file
View File

@ -0,0 +1 @@
module.exports = require("../www/common/common-signing-keys");

View File

@ -23,29 +23,13 @@ var write = function (ctx, content) {
// various degrees of logging
const logLevels = Logger.levels = ['silly', 'verbose', 'debug', 'feedback', 'info', 'warn', 'error'];
var handlers = {
silly: function (ctx, time, tag, info) {
console.log('[SILLY]', time, tag, info);
},
debug: function (ctx, time, tag, info) {
console.log('[DEBUG]', time, tag, info);
},
verbose: function (ctx, time, tag, info) {
console.log('[VERBOSE]', time, tag, info);
},
feedback: function (ctx, time, tag, info) {
console.log('[FEEDBACK]', time, tag, info);
},
info: function (ctx, time, tag, info) {
console.info('[INFO]', time, tag, info);
},
warn: function (ctx, time, tag, info) {
console.warn('[WARN]', time, tag, info);
},
error: function (ctx, time, tag, info) {
console.error('[ERROR]', time, tag, info);
}
};
var handlers = {};
['silly', 'debug', 'verbose', 'feedback', 'info'].forEach(function (level) {
handlers[level] = function (ctx, content) { console.log(content); };
});
['warn', 'error'].forEach(function (level) {
handlers[level] = function (ctx, content) { console.error(content); };
});
var noop = function () {};
@ -65,7 +49,7 @@ var createLogType = function (ctx, type) {
return;
}
if (ctx.logToStdout && typeof(handlers[type]) === 'function') {
handlers[type](ctx, time, tag, info);
handlers[type](ctx, content);
}
write(ctx, content);
};

View File

@ -27,7 +27,7 @@ const CHANNEL_WRITE_WINDOW = 300000;
them. The tradeoff with this timeout is that some functions, the stream, and
and the timeout itself are stored in memory. A longer timeout uses more memory
and running out of memory will also kill the server. */
const STREAM_CLOSE_TIMEOUT = 300000;
const STREAM_CLOSE_TIMEOUT = 120000;
/* The above timeout closes the stream, but apparently that doesn't always work.
We set yet another timeout to allow the runtime to gracefully close the stream
@ -83,15 +83,31 @@ var channelExists = function (filepath, cb) {
const destroyStream = function (stream) {
if (!stream) { return; }
try { stream.close(); } catch (err) { console.error(err); }
try {
stream.close();
if (stream.closed && stream.fd === null) { return; }
} catch (err) {
console.error(err);
}
setTimeout(function () {
try { stream.destroy(); } catch (err) { console.error(err); }
}, STREAM_DESTROY_TIMEOUT);
};
/*
accept a stream, an id (used as a label) and an optional number of milliseconds
return a function which ignores all arguments
and first tries to gracefully close a stream
then destroys it after a period if the close was not successful
if the function is not called within the specified number of milliseconds
then it will be called implicitly with an error to indicate
that it was run because it timed out
*/
const ensureStreamCloses = function (stream, id, ms) {
return Util.bake(Util.mkTimeout(Util.once(function (err) {
destroyStream(stream, id);
destroyStream(stream);
if (err) {
// this can only be a timeout error...
console.log("stream close error:", err, id);
@ -106,7 +122,7 @@ const ensureStreamCloses = function (stream, id, ms) {
// it also allows the handler to abort reading at any time
const readMessagesBin = (env, id, start, msgHandler, cb) => {
const stream = Fs.createReadStream(mkPath(env, id), { start: start });
const finish = ensureStreamCloses(stream, id);
const finish = ensureStreamCloses(stream, '[readMessagesBin:' + id + ']');
return void readFileBin(stream, msgHandler, function (err) {
cb(err);
finish();
@ -117,7 +133,7 @@ const readMessagesBin = (env, id, start, msgHandler, cb) => {
// returns undefined if the first message was not an object (not an array)
var getMetadataAtPath = function (Env, path, _cb) {
const stream = Fs.createReadStream(path, { start: 0 });
const finish = ensureStreamCloses(stream, path);
const finish = ensureStreamCloses(stream, '[getMetadataAtPath:' + path + ']');
var cb = Util.once(Util.mkAsync(Util.both(_cb, finish)), function () {
throw new Error("Multiple Callbacks");
});
@ -203,7 +219,7 @@ var clearChannel = function (env, channelId, _cb) {
*/
var readMessages = function (path, msgHandler, _cb) {
var stream = Fs.createReadStream(path, { start: 0});
const finish = ensureStreamCloses(stream, path);
const finish = ensureStreamCloses(stream, '[readMessages:' + path + ']');
var cb = Util.once(Util.mkAsync(Util.both(finish, _cb)));
return readFileBin(stream, function (msgObj, readMore) {
@ -231,7 +247,7 @@ var getDedicatedMetadata = function (env, channelId, handler, _cb) {
var metadataPath = mkMetadataPath(env, channelId);
var stream = Fs.createReadStream(metadataPath, {start: 0});
const finish = ensureStreamCloses(stream, metadataPath);
const finish = ensureStreamCloses(stream, '[getDedicatedMetadata:' + metadataPath + ']');
var cb = Util.both(finish, _cb);
readFileBin(stream, function (msgObj, readMore) {
@ -655,29 +671,6 @@ var unarchiveChannel = function (env, channelName, cb) {
}));
});
};
/*
var flushUnusedChannels = function (env, cb, frame) {
var currentTime = +new Date();
var expiration = typeof(frame) === 'undefined'? env.channelExpirationMs: frame;
Object.keys(env.channels).forEach(function (chanId) {
var chan = env.channels[chanId];
if (typeof(chan.atime) !== 'number') { return; }
if (currentTime >= expiration + chan.atime) {
closeChannel(env, chanId, function (err) {
if (err) {
console.error(err);
return;
}
if (env.verbose) {
console.log("Closed channel [%s]", chanId);
}
});
}
});
cb();
};
*/
/* channelBytes
calls back with an error or the size (in bytes) of a channel and its metadata
@ -1219,11 +1212,6 @@ module.exports.create = function (conf, _cb) {
closeChannel(env, channelName, Util.both(cb, next));
});
},
// iterate over open channels and close any that are not active
flushUnusedChannels: function (cb) {
cb("DEPRECATED");
//flushUnusedChannels(env, cb);
},
// write to a log file
log: function (channelName, content, cb) {
// you probably want the events in your log to be in the correct order.
@ -1237,8 +1225,4 @@ module.exports.create = function (conf, _cb) {
}
});
});
/*
it = setInterval(function () {
flushUnusedChannels(env, function () { });
}, 5000);*/
};

View File

@ -1,113 +0,0 @@
/* jshint esversion: 6 */
/* global process */
const Nacl = require('tweetnacl/nacl-fast');
const COMMANDS = {};
COMMANDS.INLINE = function (data, cb) {
var signedMsg;
try {
signedMsg = Nacl.util.decodeBase64(data.msg);
} catch (e) {
return void cb('E_BAD_MESSAGE');
}
var validateKey;
try {
validateKey = Nacl.util.decodeBase64(data.key);
} catch (e) {
return void cb("E_BADKEY");
}
// validate the message
const validated = Nacl.sign.open(signedMsg, validateKey);
if (!validated) {
return void cb("FAILED");
}
cb();
};
const checkDetachedSignature = function (signedMsg, signature, publicKey) {
if (!(signedMsg && publicKey)) { return false; }
var signedBuffer;
var pubBuffer;
var signatureBuffer;
try {
signedBuffer = Nacl.util.decodeUTF8(signedMsg);
} catch (e) {
throw new Error("INVALID_SIGNED_BUFFER");
}
try {
pubBuffer = Nacl.util.decodeBase64(publicKey);
} catch (e) {
throw new Error("INVALID_PUBLIC_KEY");
}
try {
signatureBuffer = Nacl.util.decodeBase64(signature);
} catch (e) {
throw new Error("INVALID_SIGNATURE");
}
if (pubBuffer.length !== 32) {
throw new Error("INVALID_PUBLIC_KEY_LENGTH");
}
if (signatureBuffer.length !== 64) {
throw new Error("INVALID_SIGNATURE_LENGTH");
}
if (Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
throw new Error("FAILED");
}
};
COMMANDS.DETACHED = function (data, cb) {
try {
checkDetachedSignature(data.msg, data.sig, data.key);
} catch (err) {
return void cb(err && err.message);
}
cb();
};
COMMANDS.HASH_CHANNEL_LIST = function (data, cb) {
var channels = data.channels;
if (!Array.isArray(channels)) { return void cb('INVALID_CHANNEL_LIST'); }
var uniques = [];
channels.forEach(function (a) {
if (uniques.indexOf(a) === -1) { uniques.push(a); }
});
uniques.sort();
var hash = Nacl.util.encodeBase64(Nacl.hash(Nacl
.util.decodeUTF8(JSON.stringify(uniques))));
cb(void 0, hash);
};
process.on('message', function (data) {
if (!data || !data.txid) {
return void process.send({
error:'E_INVAL'
});
}
const cb = function (err, value) {
process.send({
txid: data.txid,
error: err,
value: value,
});
};
const command = COMMANDS[data.command];
if (typeof(command) !== 'function') {
return void cb("E_BAD_COMMAND");
}
command(data, cb);
});

View File

@ -12,6 +12,7 @@ const Core = require("../commands/core");
const Saferphore = require("saferphore");
const Logger = require("../log");
const Tasks = require("../storage/tasks");
const Nacl = require('tweetnacl/nacl-fast');
const Env = {
Log: {},
@ -221,10 +222,10 @@ const computeMetadata = function (data, cb) {
const getOlderHistory = function (data, cb) {
const oldestKnownHash = data.hash;
const channelName = data.channel;
const desiredMessages = data.desiredMessages;
const desiredCheckpoint = data.desiredCheckpoint;
//const store = Env.store;
//const Log = Env.Log;
var messageBuffer = [];
var messages = [];
var found = false;
store.getMessages(channelName, function (msgStr) {
if (found) { return; }
@ -245,9 +246,22 @@ const getOlderHistory = function (data, cb) {
if (hash === oldestKnownHash) {
found = true;
}
messageBuffer.push(parsed);
messages.push(parsed);
}, function (err) {
cb(err, messageBuffer);
var toSend = [];
if (typeof (desiredMessages) === "number") {
toSend = messages.slice(-desiredMessages);
} else {
let cpCount = 0;
for (var i = messages.length - 1; i >= 0; i--) {
if (/^cp\|/.test(messages[i][4]) && i !== (messages.length - 1)) {
cpCount++;
}
toSend.unshift(messages[i]);
if (cpCount >= desiredCheckpoint) { break; }
}
}
cb(err, toSend);
});
};
@ -418,6 +432,10 @@ const runTasks = function (data, cb) {
Env.tasks.runAll(cb);
};
const writeTask = function (data, cb) {
Env.tasks.write(data.time, data.task_command, data.args, cb);
};
const COMMANDS = {
COMPUTE_INDEX: computeIndex,
COMPUTE_METADATA: computeMetadata,
@ -430,6 +448,92 @@ const COMMANDS = {
GET_HASH_OFFSET: getHashOffset,
REMOVE_OWNED_BLOB: removeOwnedBlob,
RUN_TASKS: runTasks,
WRITE_TASK: writeTask,
};
COMMANDS.INLINE = function (data, cb) {
var signedMsg;
try {
signedMsg = Nacl.util.decodeBase64(data.msg);
} catch (e) {
return void cb('E_BAD_MESSAGE');
}
var validateKey;
try {
validateKey = Nacl.util.decodeBase64(data.key);
} catch (e) {
return void cb("E_BADKEY");
}
// validate the message
const validated = Nacl.sign.open(signedMsg, validateKey);
if (!validated) {
return void cb("FAILED");
}
cb();
};
const checkDetachedSignature = function (signedMsg, signature, publicKey) {
if (!(signedMsg && publicKey)) { return false; }
var signedBuffer;
var pubBuffer;
var signatureBuffer;
try {
signedBuffer = Nacl.util.decodeUTF8(signedMsg);
} catch (e) {
throw new Error("INVALID_SIGNED_BUFFER");
}
try {
pubBuffer = Nacl.util.decodeBase64(publicKey);
} catch (e) {
throw new Error("INVALID_PUBLIC_KEY");
}
try {
signatureBuffer = Nacl.util.decodeBase64(signature);
} catch (e) {
throw new Error("INVALID_SIGNATURE");
}
if (pubBuffer.length !== 32) {
throw new Error("INVALID_PUBLIC_KEY_LENGTH");
}
if (signatureBuffer.length !== 64) {
throw new Error("INVALID_SIGNATURE_LENGTH");
}
if (Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
throw new Error("FAILED");
}
};
COMMANDS.DETACHED = function (data, cb) {
try {
checkDetachedSignature(data.msg, data.sig, data.key);
} catch (err) {
return void cb(err && err.message);
}
cb();
};
COMMANDS.HASH_CHANNEL_LIST = function (data, cb) {
var channels = data.channels;
if (!Array.isArray(channels)) { return void cb('INVALID_CHANNEL_LIST'); }
var uniques = [];
channels.forEach(function (a) {
if (uniques.indexOf(a) === -1) { uniques.push(a); }
});
uniques.sort();
var hash = Nacl.util.encodeBase64(Nacl.hash(Nacl
.util.decodeUTF8(JSON.stringify(uniques))));
cb(void 0, hash);
};
process.on('message', function (data) {

View File

@ -3,103 +3,14 @@
const Util = require("../common-util");
const nThen = require('nthen');
const OS = require("os");
const numCPUs = OS.cpus().length;
const { fork } = require('child_process');
const Workers = module.exports;
const PID = process.pid;
const CRYPTO_PATH = 'lib/workers/crypto-worker';
const DB_PATH = 'lib/workers/db-worker';
const MAX_JOBS = 16;
Workers.initializeValidationWorkers = function (Env) {
if (typeof(Env.validateMessage) !== 'undefined') {
return void console.error("validation workers are already initialized");
}
// Create our workers
const workers = [];
for (let i = 0; i < numCPUs; i++) {
workers.push(fork(CRYPTO_PATH));
}
const response = Util.response(function (errLabel, info) {
Env.Log.error('HK_VALIDATE_WORKER__' + errLabel, info);
});
var initWorker = function (worker) {
worker.on('message', function (res) {
if (!res || !res.txid) { return; }
response.handle(res.txid, [res.error, res.value]);
});
var substituteWorker = Util.once( function () {
Env.Log.info("SUBSTITUTE_VALIDATION_WORKER", '');
var idx = workers.indexOf(worker);
if (idx !== -1) {
workers.splice(idx, 1);
}
// Spawn a new one
var w = fork(CRYPTO_PATH);
workers.push(w);
initWorker(w);
});
// Spawn a new process in one ends
worker.on('exit', substituteWorker);
worker.on('close', substituteWorker);
worker.on('error', function (err) {
substituteWorker();
Env.Log.error('VALIDATION_WORKER_ERROR', {
error: err,
});
});
};
workers.forEach(initWorker);
var nextWorker = 0;
const send = function (msg, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
// let's be paranoid about asynchrony and only calling back once..
nextWorker = (nextWorker + 1) % workers.length;
if (workers.length === 0 || typeof(workers[nextWorker].send) !== 'function') {
return void cb("INVALID_WORKERS");
}
var txid = msg.txid = Util.uid();
// expect a response within 45s
response.expect(txid, cb, 60000);
// Send the request
workers[nextWorker].send(msg);
};
Env.validateMessage = function (signedMsg, key, cb) {
send({
msg: signedMsg,
key: key,
command: 'INLINE',
}, cb);
};
Env.checkSignature = function (signedMsg, signature, publicKey, cb) {
send({
command: 'DETACHED',
sig: signature,
msg: signedMsg,
key: publicKey,
}, cb);
};
Env.hashChannelList = function (channels, cb) {
send({
command: 'HASH_CHANNEL_LIST',
channels: channels,
}, cb);
};
};
Workers.initializeIndexWorkers = function (Env, config, _cb) {
Workers.initialize = function (Env, config, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
const workers = [];
@ -124,16 +35,72 @@ Workers.initializeIndexWorkers = function (Env, config, _cb) {
return response.expected(id)? guid(): id;
};
var workerIndex = 0;
var sendCommand = function (msg, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
const countWorkerTasks = function (/* index */) {
return 0; // XXX this disables all queueing until it can be proven correct
//return Object.keys(workers[index].tasks || {}).length;
};
workerIndex = (workerIndex + 1) % workers.length;
if (!isWorker(workers[workerIndex])) {
return void cb("NO_WORKERS");
var workerOffset = -1;
var queue = [];
var getAvailableWorkerIndex = function () {
// If there is already a backlog of tasks you can avoid some work
// by going to the end of the line
if (queue.length) { return -1; }
var L = workers.length;
if (L === 0) {
Log.error('NO_WORKERS_AVAILABLE', {
queue: queue.length,
});
return -1;
}
var state = workers[workerIndex];
// cycle through the workers once
// start from a different offset each time
// return -1 if none are available
workerOffset = (workerOffset + 1) % L;
var temp;
for (let i = 0; i < L; i++) {
temp = (workerOffset + i) % L;
/* I'd like for this condition to be more efficient
(`Object.keys` is sub-optimal) but I found some bugs in my initial
implementation stemming from a task counter variable going out-of-sync
with reality when a worker crashed and its tasks were re-assigned to
its substitute. I'm sure it can be done correctly and efficiently,
but this is a relatively easy way to make sure it's always up to date.
We'll see how it performs in practice before optimizing.
*/
if (workers[temp] && countWorkerTasks(temp) <= MAX_JOBS) {
return temp;
}
}
return -1;
};
var drained = true;
var sendCommand = function (msg, _cb) {
var index = getAvailableWorkerIndex();
var state = workers[index];
// if there is no worker available:
if (!isWorker(state)) {
// queue the message for when one becomes available
queue.push({
msg: msg,
cb: _cb,
});
if (drained) {
drained = false;
Log.debug('WORKER_QUEUE_BACKLOG', {
workers: workers.length,
});
}
return;
}
const txid = guid();
msg.txid = txid;
@ -141,14 +108,60 @@ Workers.initializeIndexWorkers = function (Env, config, _cb) {
// track which worker is doing which jobs
state.tasks[txid] = msg;
response.expect(txid, function (err, value) {
// clean up when you get a response
delete state[txid];
cb(err, value);
}, 60000);
var cb = Util.once(Util.mkAsync(Util.both(_cb, function (err /*, value */) {
if (err !== 'TIMEOUT') { return; }
// in the event of a timeout the user will receive an error
// but the state used to resend a query in the event of a worker crash
// won't be cleared. This also leaks a slot that could be used to keep
// an upper bound on the amount of parallelism for any given worker.
// if you run out of slots then the worker locks up.
delete state.tasks[txid];
})));
response.expect(txid, cb, 180000);
state.worker.send(msg);
};
var handleResponse = function (state, res) {
if (!res) { return; }
// handle log messages before checking if it was addressed to your PID
// it might still be useful to know what happened inside an orphaned worker
if (res.log) {
return void handleLog(res.log, res.label, res.info);
}
// but don't bother handling things addressed to other processes
// since it's basically guaranteed not to work
if (res.pid !== PID) {
return void Log.error("WRONG_PID", res);
}
if (!res.txid) { return; }
response.handle(res.txid, [res.error, res.value]);
delete state.tasks[res.txid];
if (!queue.length) {
if (!drained) {
drained = true;
Log.debug('WORKER_QUEUE_DRAINED', {
workers: workers.length,
});
}
return;
}
var nextMsg = queue.shift();
/* `nextMsg` was at the top of the queue.
We know that a job just finished and all of this code
is synchronous, so calling `sendCommand` should take the worker
which was just freed up. This is somewhat fragile though, so
be careful if you want to modify this block. The risk is that
we take something that was at the top of the queue and push it
to the back because the following msg took its place. OR, in an
even worse scenario, we cycle through the queue but don't run anything.
*/
sendCommand(nextMsg.msg, nextMsg.cb);
};
const initWorker = function (worker, cb) {
const txid = guid();
@ -170,19 +183,7 @@ Workers.initializeIndexWorkers = function (Env, config, _cb) {
});
worker.on('message', function (res) {
if (!res) { return; }
// handle log messages before checking if it was addressed to your PID
// it might still be useful to know what happened inside an orphaned worker
if (res.log) {
return void handleLog(res.log, res.label, res.info);
}
// but don't bother handling things addressed to other processes
// since it's basically guaranteed not to work
if (res.pid !== PID) {
return void Log.error("WRONG_PID", res);
}
response.handle(res.txid, [res.error, res.value]);
handleResponse(state, res);
});
var substituteWorker = Util.once(function () {
@ -222,7 +223,33 @@ Workers.initializeIndexWorkers = function (Env, config, _cb) {
};
nThen(function (w) {
OS.cpus().forEach(function () {
const max = config.maxWorkers;
var limit;
if (typeof(max) !== 'undefined') {
// the admin provided a limit on the number of workers
if (typeof(max) === 'number' && !isNaN(max)) {
if (max < 1) {
Log.info("INSUFFICIENT_MAX_WORKERS", max);
limit = 1;
}
limit = max;
} else {
Log.error("INVALID_MAX_WORKERS", '[' + max + ']');
}
}
var logged;
OS.cpus().forEach(function (cpu, index) {
if (limit && index >= limit) {
if (!logged) {
logged = true;
Log.info('WORKER_LIMIT', "(Opting not to use available CPUs beyond " + index + ')');
}
return;
}
initWorker(fork(DB_PATH), w(function (err) {
if (!err) { return; }
w.abort();
@ -254,12 +281,14 @@ Workers.initializeIndexWorkers = function (Env, config, _cb) {
});
};
Env.getOlderHistory = function (channel, oldestKnownHash, cb) {
Env.getOlderHistory = function (channel, oldestKnownHash, desiredMessages, desiredCheckpoint, cb) {
Env.store.getWeakLock(channel, function (next) {
sendCommand({
channel: channel,
command: "GET_OLDER_HISTORY",
hash: oldestKnownHash,
desiredMessages: desiredMessages,
desiredCheckpoint: desiredCheckpoint,
}, Util.both(next, cb));
});
};
@ -327,11 +356,42 @@ Workers.initializeIndexWorkers = function (Env, config, _cb) {
}, cb);
};
Env.writeTask = function (time, command, args, cb) {
sendCommand({
command: 'WRITE_TASK',
time: time,
task_command: command,
args: args,
}, cb);
};
// Synchronous crypto functions
Env.validateMessage = function (signedMsg, key, cb) {
sendCommand({
msg: signedMsg,
key: key,
command: 'INLINE',
}, cb);
};
Env.checkSignature = function (signedMsg, signature, publicKey, cb) {
sendCommand({
command: 'DETACHED',
sig: signature,
msg: signedMsg,
key: publicKey,
}, cb);
};
Env.hashChannelList = function (channels, cb) {
sendCommand({
command: 'HASH_CHANNEL_LIST',
channels: channels,
}, cb);
};
cb(void 0);
});
};
Workers.initialize = function (Env, config, cb) {
Workers.initializeValidationWorkers(Env);
Workers.initializeIndexWorkers(Env, config, cb);
};

View File

@ -17,7 +17,7 @@ module.exports = function () {
var next = function (id) {
setTimeout(function () {
if (map[id] && map[id].length === 0) { return void delete map[id]; }
if (!map[id] || map[id].length === 0) { return void delete map[id]; }
var task = map[id].shift();
task(fix1(next, id));
});

8
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "cryptpad",
"version": "3.15.0",
"version": "3.20.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -977,9 +977,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true
},
"lodash.clonedeep": {

View File

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "3.15.0",
"version": "3.20.1",
"license": "AGPL-3.0+",
"repository": {
"type": "git",

View File

@ -22,7 +22,7 @@ The most recent version and all past release notes can be found [here](https://g
## Setup using Docker
See [Cryptpad-Docker](docs/cryptpad-docker.md) and the community wiki's [Docker](https://github.com/xwiki-labs/cryptpad/wiki/Docker) page for details on how to get up-and-running with Cryptpad in Docker.
See [Cryptpad-Docker](https://github.com/xwiki-labs/cryptpad-docker) repository for details on how to get up-and-running with Cryptpad in Docker. This repository is maintained by the community and not officially supported.
## Setup using Ansible

View File

@ -175,8 +175,12 @@ nThen(function (w) {
Log.error("EVICT_BLOB_LIST_BLOBS_ERROR", err);
return void next();
}
if (!item) {
next();
return void Log.error('EVICT_BLOB_LIST_BLOBS_NO_ITEM', item);
}
if (pins[item.blobId]) { return void next(); }
if (item && getNewestTime(item) > retentionTime) { return void next(); }
if (getNewestTime(item) > inactiveTime) { return void next(); }
blobs.archive.blob(item.blobId, function (err) {
if (err) {
@ -204,8 +208,12 @@ nThen(function (w) {
next();
return void Log.error("EVICT_BLOB_LIST_PROOFS_ERROR", err);
}
if (!item) {
next();
return void Log.error('EVICT_BLOB_LIST_PROOFS_NO_ITEM', item);
}
if (pins[item.blobId]) { return void next(); }
if (item && getNewestTime(item) > retentionTime) { return void next(); }
if (getNewestTime(item) > inactiveTime) { return void next(); }
nThen(function (w) {
blobs.size(item.blobId, w(function (err, size) {
if (err) {

View File

@ -10,7 +10,7 @@ nThen(function (w) {
config.log = _log;
}));
}).nThen(function (w) {
FileStorage.create(config, w(function (_store) {
FileStorage.create(config, w(function (err, _store) {
config.store = _store;
// config.taskPath

116
scripts/tests/test-lkh.js Normal file
View File

@ -0,0 +1,116 @@
/* globals process */
var Client = require("../../lib/client");
var Nacl = require("tweetnacl/nacl-fast");
var nThen = require("nthen");
var CPNetflux = require("../../www/bower_components/chainpad-netflux/chainpad-netflux");
var Hash = require("../../www/common/common-hash");
var Rpc = require("../../www/common/rpc");
var HK = require("../../lib/hk-util");
var identity = function (x) {
return x;
};
var crypto = {
encrypt: identity,
decrypt: identity,
};
var N = 2;
var BREAK;
BREAK = 1;
var client;
nThen(function (w) {
//console.log("Creating client");
Client.create(w(function (err, _client) {
if (err) {
console.error(err);
process.exit(1);
}
client = _client;
}));
}).nThen(function (w) {
//console.log("Creating RPC module");
Rpc.createAnonymous(client.config.network, w(function (err, rpc) {
if (err) {
w.abort();
return void console.error('ANON_RPC_CONNECT_ERR');
}
client.anonRpc = rpc;
}));
}).nThen(function (w) {
var done = w();
//console.log("sending random messages");
client.channel = Hash.createChannelId();
if (BREAK) {
CPNetflux.start({
//lastKnownHash: HK.getHash(client.sent[0]),
network: client.config.network,
channel: client.channel,
crypto: crypto,
noChainPad: true,
onReady: w(),
//onMessage: onMessage,
});
}
// send a few random messages to a channel
client.sent = [];
var i = N;
var send = function () {
//console.log(i);
if (i-- <= 0) { return void done(); }
var ciphertext = Nacl.util.encodeBase64(Nacl.randomBytes(256));
client.anonRpc.send('WRITE_PRIVATE_MESSAGE', [
client.channel,
ciphertext
], function (err) {
if (err) {
console.error(err);
process.exit(1);
}
client.sent.push(ciphertext);
console.log("sent: %s", ciphertext);
//setTimeout(send, 500);
send();
});
};
send();
}).nThen(function () {
//process.exit(1);
// connect to that channel with a lastKnownHash
// check if the first message received has the hash that you asked for
console.log();
var lkh = HK.getHash(client.sent[0]);
var i = 0;
var onMessage = function (msg, user, vKey, isCp, hash /*, author */) {
if (i === 0 && hash !== lkh) {
console.error('incorrect hash: [%s]', hash);
process.exit(1);
}
console.log(msg);
if (++i >= N) {
process.exit(1);
}
};
CPNetflux.start({
lastKnownHash: lkh,
network: client.config.network,
channel: client.channel,
crypto: crypto,
noChainPad: true,
onMessage: onMessage,
});
});

View File

@ -9,6 +9,7 @@ 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");
@ -128,7 +129,7 @@ var setHeaders = (function () {
if (Object.keys(headers).length) {
return function (req, res) {
const h = [
/^\/pad\/inner\.html.*/,
///^\/pad\/inner\.html.*/,
/^\/common\/onlyoffice\/.*\/index\.html.*/,
/^\/(sheet|ooslide|oodoc)\/inner\.html.*/,
].some((regex) => {
@ -201,9 +202,11 @@ app.use(/^\/[^\/]*$/, Express.static('customize.dist'));
var admins = [];
try {
admins = (config.adminKeys || []).map(function (k) {
k = k.replace(/\/+$/, '');
var s = k.split('/');
return s[s.length-1].replace(/-/g, '/');
// return each admin's "unsafeKey"
// this might throw and invalidate all the other admin's keys
// but we want to get the admin's attention anyway.
// breaking everything is a good way to accomplish that.
return Keys.parseUser(k).pubkey;
});
} catch (e) { console.error("Can't parse admin keys"); }

View File

@ -32,6 +32,14 @@
.cp-support-list-message {
&:last-child:not(.cp-support-fromadmin) {
color: @colortheme_cp-red;
background-color: lighten(@colortheme_form-warning, 25%);
.cp-support-showdata {
background-color: lighten(@colortheme_form-warning, 30%);
}
}
&:last-child {
&.cp-support-frompremium {
background-color: lighten(@colortheme_cp-red, 25%);
.cp-support-showdata {
background-color: lighten(@colortheme_cp-red, 30%);
@ -39,6 +47,7 @@
}
}
}
}
.cp-support-fromadmin {
color: @colortheme_logo-2;

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
<link href="/customize/src/outer.css?ver=1.1" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

View File

@ -2,7 +2,7 @@ define([
'jquery',
'/api/config',
'/bower_components/chainpad-crypto/crypto.js',
'/common/toolbar3.js',
'/common/toolbar.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/hyperscript.js',
@ -185,6 +185,20 @@ define([
var $container = makeBlock('support-list');
var $div = $(h('div.cp-support-container')).appendTo($container);
var catContainer = h('div.cp-dropdown-container');
$div.append(catContainer);
var category = 'all';
var $drop = APP.support.makeCategoryDropdown(catContainer, function (key) {
category = key;
if (key === 'all') {
$div.find('.cp-support-list-ticket').show();
return;
}
$div.find('.cp-support-list-ticket').hide();
$div.find('.cp-support-list-ticket[data-cat="'+key+'"]').show();
}, true);
$drop.setValue('all');
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var cat = privateData.category || '';
@ -255,18 +269,31 @@ define([
if (msg.type !== 'TICKET') { return; }
if (!$ticket.length) {
$ticket = APP.support.makeTicket($div, content, function () {
$ticket = APP.support.makeTicket($div, content, function (hideButton) {
// the ticket will still be displayed until the worker confirms its deletion
// so make it unclickable in the meantime
hideButton.setAttribute('disabled', true);
var error = false;
nThen(function (w) {
hashesById[id].forEach(function (d) {
common.mailbox.dismiss(d, function (err) {
common.mailbox.dismiss(d, w(function (err) {
if (err) {
error = true;
console.error(err);
}
}));
});
}).nThen(function () {
if (!error) { return void $ticket.remove(); }
// if deletion failed then reactivate the button and warn
hideButton.removeAttribute('disabled');
// and show a generic error message
UI.alert(Messages.error);
});
});
if (!error) { $ticket.remove(); }
});
if (category !== 'all' && $ticket.attr('data-cat') !== category) {
$ticket.hide();
}
}
$ticket.append(APP.support.makeMessage(content, hash));
reorder();

View File

@ -105,6 +105,12 @@
.markdown_preformatted-code;
.markdown_gfm-table(black);
table {
margin-bottom: 1rem;
}
media-tag > * {
margin-bottom: 1rem;
}
}
.cp-splitter {

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="/common/sframe-app-outer.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
<link href="/customize/src/outer.css?ver=1.1" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

View File

@ -3,16 +3,19 @@ define([
'/common/diffMarked.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/hyperscript.js',
'/common/sframe-app-framework.js',
'/common/sframe-common-codemirror.js',
'/common/common-interface.js',
'/common/common-util.js',
'/common/common-hash.js',
'/common/modes.js',
'/code/markers.js',
'/common/visible.js',
'/common/TypingTests.js',
'/customize/messages.js',
'cm/lib/codemirror',
'css!cm/lib/codemirror.css',
'css!cm/addon/dialog/dialog.css',
'css!cm/addon/fold/foldgutter.css',
@ -46,11 +49,13 @@ define([
DiffMd,
nThen,
SFCommon,
h,
Framework,
SFCodeMirror,
UI,
Util,
Hash,
Modes,
Markers,
Visible,
TypingTest,
Messages,
@ -70,6 +75,31 @@ define([
'xml',
]);
var mkThemeButton = function (framework) {
var $theme = $(h('button.cp-toolbar-appmenu', [
h('i.cptools.cptools-palette'),
h('span.cp-button-name', Messages.toolbar_theme)
]));
var $content = $(h('div.cp-toolbar-drawer-content', {
tabindex: 1
})).hide();
// set up all the necessary events
UI.createDrawer($theme, $content);
framework._.toolbar.$theme = $content;
framework._.toolbar.$bottomL.append($theme);
};
var mkCbaButton = function (framework, markers) {
var $showAuthorColorsButton = framework._.sfCommon.createButton('', true, {
text: Messages.cba_hide,
name: 'authormarks',
icon: 'fa-paint-brush',
}).hide();
framework._.toolbar.$theme.append($showAuthorColorsButton);
markers.setButton($showAuthorColorsButton);
};
var mkPrintButton = function (framework, $content, $print) {
var $printButton = framework._.sfCommon.createButton('print', true);
$printButton.click(function () {
@ -85,7 +115,7 @@ define([
var markdownTb = framework._.sfCommon.createMarkdownToolbar(editor);
$codeMirrorContainer.prepend(markdownTb.toolbar);
framework._.toolbar.$rightside.append(markdownTb.button);
framework._.toolbar.$bottomL.append(markdownTb.button);
var modeChange = function (mode) {
if (['markdown', 'gfm'].indexOf(mode) !== -1) { return void markdownTb.setState(true); }
@ -161,7 +191,7 @@ define([
}
});
framework._.toolbar.$rightside.append($previewButton);
framework._.toolbar.$bottomM.append($previewButton);
$preview.click(function (e) {
if (!e.target) { return; }
@ -265,6 +295,8 @@ define([
}
});
DiffMd.onPluginLoaded(drawPreview);
return {
forceDraw: forceDrawPreview,
draw: drawPreview,
@ -273,11 +305,60 @@ define([
};
};
var mkColorByAuthor = function (framework, markers) {
var common = framework._.sfCommon;
var $cbaButton = framework._.sfCommon.createButton(null, true, {
icon: 'fa-paint-brush',
text: Messages.cba_title,
name: 'cba'
}, function () {
var div = h('div');
var $div = $(div);
var content = h('div', [
h('h4', Messages.cba_properties),
h('p', Messages.cba_hint),
div
]);
var setButton = function (state) {
var button = h('button.btn');
var $button = $(button);
$div.html('').append($button);
if (state) {
// Add "enable" button
$button.addClass('btn-secondary').text(Messages.cba_enable);
UI.confirmButton(button, {
classes: 'btn-primary'
}, function () {
$button.remove();
markers.setState(true);
common.setAttribute(['code', 'enableColors'], true);
setButton(false);
});
return;
}
// Add "disable" button
$button.addClass('btn-danger-alt').text(Messages.cba_disable);
UI.confirmButton(button, {
classes: 'btn-danger'
}, function () {
$button.remove();
markers.setState(false);
common.setAttribute(['code', 'enableColors'], false);
setButton(true);
});
};
setButton(!markers.getState());
UI.alert(content);
});
framework._.toolbar.$theme.append($cbaButton);
};
var mkFilePicker = function (framework, editor, evModeChange) {
evModeChange.reg(function (mode) {
if (MEDIA_TAG_MODES.indexOf(mode) !== -1) {
// Embedding is endabled
framework.setMediaTagEmbedder(function (mt) {
editor.focus();
editor.replaceSelection($(mt)[0].outerHTML);
});
} else {
@ -300,11 +381,24 @@ define([
var previewPane = mkPreviewPane(editor, CodeMirror, framework, isPresentMode);
var markdownTb = mkMarkdownTb(editor, framework);
mkThemeButton(framework);
var markers = Markers.create({
common: common,
framework: framework,
CodeMirror: CodeMirror,
devMode: privateData.devMode,
editor: editor
});
mkCbaButton(framework, markers);
var $print = $('#cp-app-code-print');
var $content = $('#cp-app-code-preview-content');
mkPrintButton(framework, $content, $print);
if (!privateData.isEmbed) {
mkHelpMenu(framework);
}
var evModeChange = Util.mkEvent();
evModeChange.reg(previewPane.modeChange);
@ -322,15 +416,23 @@ define([
CodeMirror.configureTheme(common);
}
////
framework.onContentUpdate(function (newContent) {
var highlightMode = newContent.highlightMode;
if (highlightMode && highlightMode !== CodeMirror.highlightMode) {
CodeMirror.setMode(highlightMode, evModeChange.fire);
}
// Fix the markers offsets
markers.checkMarks(newContent);
// Apply the text content
CodeMirror.contentUpdate(newContent);
previewPane.draw();
// Apply the markers
markers.setMarks();
framework.localChange();
});
framework.setContentGetter(function () {
@ -338,6 +440,10 @@ define([
var content = CodeMirror.getContent();
content.highlightMode = CodeMirror.highlightMode;
previewPane.draw();
markers.updateAuthorMarks();
content.authormarks = markers.getAuthorMarks();
return content;
});
@ -367,6 +473,19 @@ define([
//console.log("%s => %s", CodeMirror.highlightMode, CodeMirror.$language.val());
}
markers.ready();
common.getPadMetadata(null, function (md) {
if (md && md.error) { return; }
if (!Array.isArray(md.owners)) { return void markers.setState(false); }
if (!common.isOwned(md.owners)) { return; }
// We're the owner: add the button and enable the colors if needed
mkColorByAuthor(framework, markers);
if (newPad && Util.find(privateData, ['settings', 'code', 'enableColors'])) {
markers.setState(true);
}
});
var fmConfig = {
dropArea: $('.CodeMirror'),
body: $('body'),
@ -384,7 +503,7 @@ define([
});
framework.onDefaultContentNeeded(function () {
editor.setValue(''); //Messages.codeInitialState);
editor.setValue('');
});
framework.setFileExporter(CodeMirror.getContentExtension, CodeMirror.fileExporter);
@ -401,11 +520,14 @@ define([
framework.setNormalizer(function (c) {
return {
content: c.content,
highlightMode: c.highlightMode
highlightMode: c.highlightMode,
authormarks: c.authormarks
};
});
editor.on('change', framework.localChange);
editor.on('change', function( cm, change ) {
markers.localChange(change, framework.localChange);
});
framework.start();

726
www/code/markers.js Normal file
View File

@ -0,0 +1,726 @@
define([
'/common/common-util.js',
'/common/sframe-common-codemirror.js',
'/customize/messages.js',
'/bower_components/chainpad/chainpad.dist.js',
], function (Util, SFCodeMirror, Messages, ChainPad) {
var Markers = {};
/* TODO Known Issues
* 1. ChainPad diff is not completely accurate: we're not aware of the other user's cursor
position so if they insert an "a" in the middle of "aaaaa", the diff will think that
the "a" was inserted at the end of this sequence. This is not an issue for the content
but it will cause issues for the colors
2. ChainPad doesn't always provide the good result in case of conflict (?)
e.g. Alice is inserting "pew" at offset 10, Bob is removing 1 character at offset 10
The expected result is to have "pew" and the following character deleted
In some cases, the result is "ew" inserted and the following character not deleted
*/
var debug = function () {};
var MARK_OPACITY = 0.5;
var DEFAULT = {
authors: {},
marks: [[-1, 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]]
};
var addMark = function (Env, from, to, uid) {
if (!Env.enabled) { return; }
var author = Env.authormarks.authors[uid] || {};
if (uid === -1) {
return void Env.editor.markText(from, to, {
css: "background-color: transparent",
attributes: {
'data-type': 'authormark',
'data-uid': uid
}
});
}
uid = Number(uid);
var name = Util.fixHTML(author.name || Messages.anonymous);
var col = Util.hexToRGB(author.color);
var rgba = 'rgba('+col[0]+','+col[1]+','+col[2]+','+Env.opacity+');';
return Env.editor.markText(from, to, {
inclusiveLeft: uid === Env.myAuthorId,
inclusiveRight: uid === Env.myAuthorId,
css: "background-color: " + rgba,
attributes: {
title: Env.opacity ? Messages._getKey('cba_writtenBy', [name]) : '',
'data-type': 'authormark',
'data-uid': uid
}
});
};
var sortMarks = function (a, b) {
if (!Array.isArray(b)) { return -1; }
if (!Array.isArray(a)) { return 1; }
// Check line
if (a[1] < b[1]) { return -1; }
if (a[1] > b[1]) { return 1; }
// Same line: check start offset
if (a[2] < b[2]) { return -1; }
if (a[2] > b[2]) { return 1; }
return 0;
};
/* Formats:
[uid, startLine, startCh, endLine, endCh] (multi line)
[uid, startLine, startCh, endCh] (single line)
[uid, startLine, startCh] (single character)
*/
var parseMark = Markers.parseMark = function (array) {
if (!Array.isArray(array)) { return {}; }
var multiline = typeof(array[4]) !== "undefined";
var singleChar = typeof(array[3]) === "undefined";
return {
uid: array[0],
startLine: array[1],
startCh: array[2],
endLine: multiline ? array[3] : array[1],
endCh: singleChar ? (array[2]+1) : (multiline ? array[4] : array[3])
};
};
var setAuthorMarks = function (Env, authormarks) {
if (!Env.enabled) {
Env.authormarks = {};
return;
}
authormarks = authormarks || {};
if (!authormarks.marks) { authormarks.marks = Util.clone(DEFAULT.marks); }
if (!authormarks.authors) { authormarks.authors = Util.clone(DEFAULT.authors); }
Env.oldMarks = Env.authormarks;
Env.authormarks = authormarks;
};
var getAuthorMarks = function (Env) {
return Env.authormarks;
};
var updateAuthorMarks = function (Env) {
if (!Env.enabled) { return; }
// get author marks
var _marks = [];
var all = [];
var i = 0;
Env.editor.getAllMarks().forEach(function (mark) {
var pos = mark.find();
var attributes = mark.attributes || {};
if (!pos || attributes['data-type'] !== 'authormark') { return; }
var uid = Number(attributes['data-uid']) || 0;
all.forEach(function (obj) {
if (obj.uid !== uid) { return; }
if (obj.removed) { return; }
// Merge left
if (obj.pos.to.line === pos.from.line && obj.pos.to.ch === pos.from.ch) {
obj.removed = true;
_marks[obj.index] = undefined;
obj.mark.clear();
mark.clear();
mark = addMark(Env, obj.pos.from, pos.to, uid);
pos.from = obj.pos.from;
return;
}
// Merge right
if (obj.pos.from.line === pos.to.line && obj.pos.from.ch === pos.to.ch) {
obj.removed = true;
_marks[obj.index] = undefined;
obj.mark.clear();
mark.clear();
mark = addMark(Env, pos.from, obj.pos.to, uid);
pos.to = obj.pos.to;
}
});
var array = [uid, pos.from.line, pos.from.ch];
if (pos.from.line === pos.to.line && pos.to.ch > (pos.from.ch+1)) {
// If there is more than 1 character, add the "to" character
array.push(pos.to.ch);
} else if (pos.from.line !== pos.to.line) {
// If the mark is on more than one line, add the "to" line data
Array.prototype.push.apply(array, [pos.to.line, pos.to.ch]);
}
_marks.push(array);
all.push({
uid: uid,
pos: pos,
mark: mark,
index: i
});
i++;
});
_marks.sort(sortMarks);
debug('warn', _marks);
Env.authormarks.marks = _marks.filter(Boolean);
};
// Fix all marks located after the given operation in the provided document
var fixMarksFromOp = function (Env, op, marks, doc) {
var pos = SFCodeMirror.posToCursor(op.offset, doc); // pos of start offset
var rPos = SFCodeMirror.posToCursor(op.offset + op.toRemove, doc); // end of removed content
var removed = doc.slice(op.offset, op.offset + op.toRemove).split('\n'); // removed content
var added = op.toInsert.split('\n'); // added content
var posEndLine = pos.line + added.length - 1; // end line after op
var posEndCh = added[added.length - 1].length; // end ch after op
var addLine = added.length - removed.length;
var addCh = added[added.length - 1].length - removed[removed.length - 1].length;
if (addLine > 0) { addCh -= pos.ch; }
else if (addLine < 0) { addCh += pos.ch; }
else { posEndCh += pos.ch; }
var splitted;
marks.forEach(function (mark, i) {
if (!mark) { return; }
var p = parseMark(mark);
// Don't update marks located before the operation
if (p.endLine < pos.line || (p.endLine === pos.line && p.endCh < pos.ch)) { return; }
// Remove markers that have been deleted by my changes
if ((p.startLine > pos.line || (p.startLine === pos.line && p.startCh >= pos.ch)) &&
(p.endLine < rPos.line || (p.endLine === rPos.line && p.endCh <= rPos.ch))) {
marks[i] = undefined;
return;
}
// Update markers that have been cropped right
if (p.endLine < rPos.line || (p.endLine === rPos.line && p.endCh <= rPos.ch)) {
mark[3] = pos.line;
mark[4] = pos.ch;
return;
}
// Update markers that have been cropped left. This markers will be affected by
// my toInsert so don't abort
if (p.startLine < rPos.line || (p.startLine === rPos.line && p.startCh < rPos.ch)) {
// If our change will split an existing mark, put the existing mark after the change
// and create a new mark before
if (p.startLine < pos.line || (p.startLine === pos.line && p.startCh < pos.ch)) {
splitted = [mark[0], mark[1], mark[2], pos.line, pos.ch];
}
mark[1] = rPos.line;
mark[2] = rPos.ch;
}
// Apply my toInsert the to remaining marks
mark[1] += addLine;
if (typeof(mark[4]) !== "undefined") { mark[3] += addLine; }
if (mark[1] === posEndLine) {
mark[2] += addCh;
if (typeof(mark[4]) === "undefined" && typeof(mark[3]) !== "undefined") {
mark[3] += addCh;
} else if (typeof(mark[4]) !== "undefined" && mark[3] === posEndLine) {
mark[4] += addCh;
}
}
});
if (op.toInsert.length) {
marks.push([Env.myAuthorId, pos.line, pos.ch, posEndLine, posEndCh]);
}
if (splitted) {
marks.push(splitted);
}
marks.sort(sortMarks);
};
// Remove marks added by OT and fix the incorrect ones
// first: data about the change with the lowest offset
// last: data about the change with the latest offset
// in the comments, "I" am "first"
var fixMarks = function (Env, first, last, content, toKeepEnd) {
var toKeep = [];
var toJoin = {};
debug('error', "Fix marks");
debug('warn', first);
debug('warn', last);
if (first.me !== last.me) {
// Get their start position compared to the authDoc
var lastAuthOffset = last.offset + last.total;
var lastAuthPos = SFCodeMirror.posToCursor(lastAuthOffset, last.doc);
// Get their start position compared to the localDoc
var lastLocalOffset = last.offset + first.total;
var lastLocalPos = SFCodeMirror.posToCursor(lastLocalOffset, first.doc);
// Keep their changes in the marks (after their offset)
last.marks.some(function (array, i) {
var p = parseMark(array);
// End of the mark before offset? ignore
if (p.endLine < lastAuthPos.line) { return; }
// Take everything from the first mark ending after the pos
if (p.endLine > lastAuthPos.line || p.endCh >= lastAuthPos.ch) {
toKeep = last.marks.slice(i);
last.marks.splice(i);
return true;
}
});
// Keep my marks (based on currentDoc) before their changes
first.marks.some(function (array, i) {
var p = parseMark(array);
// End of the mark before offset? ignore
if (p.endLine < lastLocalPos.line) { return; }
// Take everything from the first mark ending after the pos
if (p.endLine > lastLocalPos.line || p.endCh >= lastLocalPos.ch) {
first.marks.splice(i);
return true;
}
});
}
// If we still have markers in "first", store the last one so that we can "join"
// everything at the end
if (first.marks.length) {
var toJoinMark = first.marks[first.marks.length - 1].slice();
toJoin = parseMark(toJoinMark);
}
// Add the new markers to the result
Array.prototype.unshift.apply(toKeepEnd, toKeep);
debug('warn', toJoin);
debug('warn', toKeep);
debug('warn', toKeepEnd);
// Fix their offset: compute added lines and added characters on the last line
// using the chainpad operation data (toInsert and toRemove)
var pos = SFCodeMirror.posToCursor(first.offset, content);
var removed = content.slice(first.offset, first.offset + first.toRemove).split('\n');
var added = first.toInsert.split('\n');
var posEndLine = pos.line + added.length - 1; // end line after op
var addLine = added.length - removed.length;
var addCh = added[added.length - 1].length - removed[removed.length - 1].length;
if (addLine > 0) { addCh -= pos.ch; }
if (addLine < 0) { addCh += pos.ch; }
toKeepEnd.forEach(function (array) {
// Push to correct lines
array[1] += addLine;
if (typeof(array[4]) !== "undefined") { array[3] += addLine; }
// If they have markers on my end line, push their "ch"
if (array[1] === posEndLine) {
array[2] += addCh;
// If they have no end line, it means end line === start line,
// so we also push their end offset
if (typeof(array[4]) === "undefined" && typeof(array[3]) !== "undefined") {
array[3] += addCh;
} else if (typeof(array[4]) !== "undefined" && array[3] === posEndLine) {
array[4] += addCh;
}
}
});
if (toKeep.length && toJoin && typeof(toJoin.endLine) !== "undefined"
&& typeof(toJoin.endCh) !== "undefined") {
// Make sure the marks are joined correctly:
// fix the start position of the marks to keep
// Note: we must preserve the same end for this mark if it was single line!
if (typeof(toKeepEnd[0][4]) === "undefined") { // Single line
toKeepEnd[0][4] = toKeepEnd[0][3] || (toKeepEnd[0][2]+1); // preserve end ch
toKeepEnd[0][3] = toKeepEnd[0][1]; // preserve end line
}
toKeepEnd[0][1] = toJoin.endLine;
toKeepEnd[0][2] = toJoin.endCh;
}
debug('log', 'Fixed');
debug('warn', toKeepEnd);
};
var checkMarks = function (Env, userDoc) {
var chainpad = Env.framework._.cpNfInner.chainpad;
var editor = Env.editor;
var CodeMirror = Env.CodeMirror;
Env.enabled = Boolean(userDoc.authormarks && userDoc.authormarks.marks);
setAuthorMarks(Env, userDoc.authormarks);
if (!Env.enabled) { return; }
debug('error', 'Check marks');
var authDoc = JSON.parse(chainpad.getAuthDoc() || '{}');
if (!authDoc.content || !userDoc.content) { return; }
var authPatch = chainpad.getAuthBlock();
if (authPatch.isFromMe) {
debug('log', 'Switch branch, from me');
debug('log', authDoc.content);
debug('log', authDoc.authormarks.marks);
debug('log', userDoc.content);
// We're switching to a different branch that was created by us.
// We can't trust localDoc anymore because it contains data from the other branch
// It means the only changes that we need to consider are ours.
// Diff between userDoc and authDoc to see what we changed
var _myOps = ChainPad.Diff.diff(authDoc.content, userDoc.content).reverse();
var authormarks = Util.clone(authDoc.authormarks);
_myOps.forEach(function (op) {
fixMarksFromOp(Env, op, authormarks.marks, authDoc.content);
});
authormarks.marks = authormarks.marks.filter(Boolean);
debug('log', 'Fixed marks');
debug('warn', authormarks.marks);
setAuthorMarks(Env, authormarks);
return;
}
var oldMarks = Env.oldMarks;
if (authDoc.content === userDoc.content) { return; } // No uncommitted work
if (!userDoc.authormarks || !Array.isArray(userDoc.authormarks.marks)) { return; }
debug('warn', 'Begin...');
var localDoc = CodeMirror.canonicalize(editor.getValue());
var commonParent = chainpad.getAuthBlock().getParent().getContent().doc;
var content = JSON.parse(commonParent || '{}').content || '';
var theirOps = ChainPad.Diff.diff(content, authDoc.content);
var myOps = ChainPad.Diff.diff(content, localDoc);
debug('log', theirOps);
debug('log', myOps);
if (!myOps.length || !theirOps.length) { return; }
// If I have uncommited content when receiving a remote patch, all the operations
// placed after someone else's changes will create marker issues. We have to fix it
var sorted = [];
var myTotal = 0;
var theirTotal = 0;
var parseOp = function (me) {
return function (op) {
var size = (op.toInsert.length - op.toRemove);
sorted.push({
me: me,
offset: op.offset,
toInsert: op.toInsert,
toRemove: op.toRemove,
size: size,
marks: (me ? (oldMarks && oldMarks.marks)
: (authDoc.authormarks && authDoc.authormarks.marks)) || [],
doc: me ? localDoc : authDoc.content
});
if (me) { myTotal += size; }
else { theirTotal += size; }
};
};
myOps.forEach(parseOp(true));
theirOps.forEach(parseOp(false));
// Sort the operation in reverse order of offset
// If an operation from them has the same offset than an operation from me, put mine first
sorted.sort(function (a, b) {
if (a.offset === b.offset) {
return a.me ? -1 : 1;
}
return b.offset - a.offset;
});
debug('log', sorted);
// We start from the end so that we don't have to fix the offsets everytime
var prev;
var toKeepEnd = [];
sorted.forEach(function (op) {
// Not the same author? fix!
if (prev) {
// Provide the new "totals"
prev.total = prev.me ? myTotal : theirTotal;
op.total = op.me ? myTotal : theirTotal;
// Fix the markers
fixMarks(Env, op, prev, content, toKeepEnd);
}
if (op.me) { myTotal -= op.size; }
else { theirTotal -= op.size; }
prev = op;
});
debug('log', toKeepEnd);
// We now have all the markers located after the first operation (ordered by offset).
// Prepend the markers placed before this operation
var first = sorted[sorted.length - 1];
if (first) { Array.prototype.unshift.apply(toKeepEnd, first.marks); }
// Commit our new markers
Env.authormarks.marks = toKeepEnd;
debug('warn', toKeepEnd);
debug('warn', '...End');
};
// Reset marks displayed in CodeMirror to the marks stored in Env
var setMarks = function (Env) {
// on remote update: remove all marks, add new marks if colors are enabled
Env.editor.getAllMarks().forEach(function (marker) {
if (marker.attributes && marker.attributes['data-type'] === 'authormark') {
marker.clear();
}
});
if (!Env.enabled) { return; }
debug('error', 'setMarks');
debug('log', Env.authormarks.marks);
var authormarks = Env.authormarks;
authormarks.marks.forEach(function (mark) {
var uid = mark[0];
if (uid !== -1 && (!authormarks.authors || !authormarks.authors[uid])) { return; }
var from = {};
var to = {};
from.line = mark[1];
from.ch = mark[2];
if (mark.length === 3) {
to.line = mark[1];
to.ch = mark[2]+1;
} else if (mark.length === 4) {
to.line = mark[1];
to.ch = mark[3];
} else if (mark.length === 5) {
to.line = mark[3];
to.ch = mark[4];
}
// Remove marks that are placed under this one
try {
Env.editor.findMarks(from, to).forEach(function (mark) {
if (!mark || !mark.attributes || mark.attributes['data-type'] !== 'authormark') { return; }
mark.clear();
});
} catch (e) {
console.warn(mark, JSON.stringify(authormarks.marks));
console.error(from, to);
console.error(e);
}
addMark(Env, from, to, uid);
});
};
var setMyData = function (Env) {
if (!Env.enabled) { return; }
var userData = Env.common.getMetadataMgr().getUserData();
var old = Env.authormarks.authors[Env.myAuthorId];
Env.authormarks.authors[Env.myAuthorId] = {
name: userData.name,
curvePublic: userData.curvePublic,
color: userData.color
};
if (!old || (old.name === userData.name && old.color === userData.color)) { return; }
return true;
};
var localChange = function (Env, change, cb) {
cb = cb || function () {};
if (!Env.enabled) { return void cb(); }
debug('error', 'Local change');
debug('log', change, true);
if (change.origin === "setValue") {
// If the content is changed from a remote patch, we call localChange
// in "onContentUpdate" directly
return;
}
if (change.text === undefined || ['+input', 'paste'].indexOf(change.origin) === -1) {
return void cb();
}
// add new author mark if text is added. marks from removed text are removed automatically
// change.to is not always correct, fix it!
var to_add = {
line: change.from.line + change.text.length-1,
};
if (change.text.length > 1) {
// Multiple lines => take the length of the text added to the last line
to_add.ch = change.text[change.text.length-1].length;
} else {
// Single line => use the "from" position and add the length of the text
to_add.ch = change.from.ch + change.text[change.text.length-1].length;
}
// If my text is inside an existing mark:
// * if it's my mark, do nothing
// * if it's someone else's mark, break it
// We can only have one author mark at a given position, but there may be
// another mark (cursor selection...) at this position so we use ".some"
var toSplit, abort;
Env.editor.findMarks(change.from, to_add).some(function (mark) {
if (!mark.attributes) { return; }
if (mark.attributes['data-type'] !== 'authormark') { return; }
if (mark.attributes['data-uid'] !== Env.myAuthorId) {
toSplit = {
mark: mark,
uid: mark.attributes['data-uid']
};
} else {
// This is our mark: abort to avoid making a new one
abort = true;
}
return true;
});
if (abort) { return void cb(); }
// Add my data to the doc if it's missing
if (!Env.authormarks.authors[Env.myAuthorId]) {
setMyData(Env);
}
if (toSplit && toSplit.mark && typeof(toSplit.uid) !== "undefined") {
// Break the other user's mark if needed
var _pos = toSplit.mark.find();
toSplit.mark.clear();
addMark(Env, _pos.from, change.from, toSplit.uid); // their mark, 1st part
addMark(Env, change.from, to_add, Env.myAuthorId); // my mark
addMark(Env, to_add, _pos.to, toSplit.uid); // their mark, 2nd part
} else {
// Add my mark
addMark(Env, change.from, to_add, Env.myAuthorId);
}
cb();
};
var setButton = function (Env, $button) {
var toggle = function () {
if (Env.opacity) {
Env.opacity = 0;
$button.find('.cp-toolbar-drawer-element').text(Messages.cba_show);
$button.removeClass("cp-toolbar-button-active");
} else {
Env.opacity = MARK_OPACITY;
$button.find('.cp-toolbar-drawer-element').text(Messages.cba_hide);
$button.addClass("cp-toolbar-button-active");
}
};
toggle();
Env.$button = $button;
$button.click(function() {
toggle();
setMarks(Env);
});
};
var getAuthorId = function (Env) {
var userData = Env.common.getMetadataMgr().getUserData();
return Env.common.getAuthorId(Env.authormarks.authors, userData.curvePublic);
};
var ready = function (Env) {
Env.ready = true;
Env.myAuthorId = getAuthorId(Env);
if (!Env.enabled) { return; }
if (Env.$button) { Env.$button.show(); }
if (!Env.authormarks.marks || !Env.authormarks.marks.length) {
Env.authormarks = Util.clone(DEFAULT);
}
setMarks(Env);
};
var getState = function (Env) {
return Boolean(Env.authormarks && Env.authormarks.marks);
};
var setState = function (Env, enabled) {
// If the state has changed in the pad, change the Env too
if (!Env.ready) { return; }
if (Env.enabled === enabled) { return; }
Env.enabled = enabled;
if (!Env.enabled) {
// Reset marks
Env.authormarks = {};
setMarks(Env);
if (Env.$button) { Env.$button.hide(); }
} else {
Env.myAuthorId = getAuthorId(Env);
// If it's a reset, add initial marker
if (!Env.authormarks.marks || !Env.authormarks.marks.length) {
Env.authormarks = Util.clone(DEFAULT);
setMarks(Env);
}
if (Env.$button) { Env.$button.show(); }
}
if (Env.ready) { Env.framework.localChange(); }
};
Markers.create = function (config) {
var Env = config;
Env.authormarks = {};
Env.enabled = false;
Env.myAuthorId = 0;
if (Env.devMode) {
debug = function (level, obj, logObject) {
var f = console.log;
if (typeof(console[level]) === "function") {
f = console[level];
}
if (logObject) { return void f(obj); }
};
}
var metadataMgr = Env.common.getMetadataMgr();
metadataMgr.onChange(function () {
// If the markers are disabled or if I haven't pushed content since the last reset,
// don't update my data
if (!Env.enabled || !Env.myAuthorId || !Env.authormarks.authors ||
!Env.authormarks.authors[Env.myAuthorId]) {
return;
}
// Update my data
var changed = setMyData(Env);
if (changed) {
setMarks(Env);
Env.framework.localChange();
}
});
var call = function (f) {
return function () {
try {
[].unshift.call(arguments, Env);
return f.apply(null, arguments);
} catch (e) {
console.error(e);
}
};
};
return {
addMark: call(addMark),
getAuthorMarks: call(getAuthorMarks),
updateAuthorMarks: call(updateAuthorMarks),
checkMarks: call(checkMarks),
setMarks: call(setMarks),
localChange: call(localChange),
ready: call(ready),
setButton: call(setButton),
getState: call(getState),
setState: call(setState),
};
};
return Markers;
});

View File

@ -4,10 +4,40 @@ define([
], function (CodeMirror) {
CodeMirror.__mode = 'orgmode';
var isEmpty = function (el, idx) {
if (idx < 2) { return true; }
return !Boolean(el);
};
var onLevelOne = function (matches) {
// If all the elements starting from index 2 are empty, remove them
// because it means it's an empty header for now and it may break codemirror
if (matches && matches.length > 2 && matches.every(isEmpty)) {
matches.splice(2, (matches.length-2));
}
return ["header level1 org-level-star","header level1 org-todo","header level1 org-done", "header level1 org-priority", "header level1", "header level1 void", "header level1 comment"];
};
// Dirty hack to make the function also work as an array
onLevelOne().forEach(function (str, i) { onLevelOne[i] = str; });
var onLevelStar = function (matches) {
// If all the elements starting from index 2 are empty, remove them
// because it means it's an empty header for now and it may break codemirror
if (matches && matches.length > 2 && matches.every(isEmpty)) {
matches.splice(2, (matches.length-2));
}
return ["header org-level-star","header org-todo","header org-done", "header org-priority", "header", "header void", "header comment"];
};
// Dirty hack to make the function also work as an array
onLevelStar().forEach(function (str, i) { onLevelStar[i] = str; });
CodeMirror.defineSimpleMode("orgmode", {
start: [
{regex: /(\*\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: onLevelOne},
{regex: /(\*{1,}\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/g, sol: true, token: onLevelStar},
/*
{regex: /(\*\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header level1 org-level-star","header level1 org-todo","header level1 org-done", "header level1 org-priority", "header level1", "header level1 void", "header level1 comment"]},
{regex: /(\*{1,}\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header org-level-star","header org-todo","header org-done", "header org-priority", "header", "header void", "header comment"]},
{regex: /(\*{1,}\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/g, sol: true, token: ["header org-level-star","header org-todo","header org-done", "header org-priority", "header", "header void", "header comment"]},
*/
{regex: /(\+[^\+]+\+)/, token: ["strikethrough"]},
{regex: /(\*[^\*]+\*)/, token: ["strong"]},
{regex: /(\/[^\/]+\/)/, token: ["em"]},
@ -93,6 +123,203 @@ define([
};
});
CodeMirror.registerHelper("orgmode", "init", function (editor) {
editor.setOption("extraKeys", {
"Tab": function(cm) { org_cycle(cm); },
"Shift-Tab": function(cm){ org_shifttab(cm); },
"Alt-Left": function(cm){ org_metaleft(cm); },
"Alt-Right": function(cm){ org_metaright(cm); },
"Alt-Enter": function(cm){ org_meta_return(cm); },
"Alt-Up": function(cm){ org_metaup(cm); },
"Alt-Down": function(cm){ org_metadown(cm); },
"Shift-Alt-Left": function(cm){ org_shiftmetaleft(cm); },
"Shift-Alt-Right": function(cm){ org_shiftmetaright(cm); },
"Shift-Alt-Enter": function(cm){ org_insert_todo_heading(cm); },
"Shift-Left": function(cm){ org_shiftleft(cm); },
"Shift-Right": function(cm){ org_shiftright(cm); }
});
editor.on('mousedown', toggleHandler);
editor.on('touchstart', toggleHandler);
editor.on('gutterClick', foldLine);
// fold everything except headers by default
editor.operation(function() {
for (var i = 0; i < editor.lineCount() ; i++) {
if(/header/.test(editor.getTokenTypeAt(CodeMirror.Pos(i, 0))) === false){
fold(editor, CodeMirror.Pos(i, 0));
}
}
});
return CodeMirror.orgmode.destroy.bind(this, editor);
});
CodeMirror.registerHelper("orgmode", "destroy", function (editor) {
editor.off('mousedown', toggleHandler);
editor.off('touchstart', toggleHandler);
editor.off('gutterClick', foldLine);
// Restore CryptPad shortcuts
if (typeof (editor.updateSettings) === "function") { editor.updateSettings(); }
});
function foldLine (cm, line){
var cursor = {line: line, ch: 0};
isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor);
}
var widgets = [];
function toggleHandler (cm, e){
var position = cm.coordsChar({
left: e.clientX || (e.targetTouches && e.targetTouches[0].clientX),
top: e.clientY || (e.targetTouches && e.targetTouches[0].clientY)
}, "page"),
token = cm.getTokenAt(position);
_disableSelection();
if(/org-level-star/.test(token.type)){
_preventIfShould();
_foldHeadline();
_disableSelection();
}else if(/org-toggle/.test(token.type)){
_preventIfShould();
_toggleCheckbox();
_disableSelection();
}else if(/org-todo/.test(token.type)){
_preventIfShould();
_toggleTodo();
_disableSelection();
}else if(/org-done/.test(token.type)){
_preventIfShould();
_toggleDone();
_disableSelection();
}else if(/org-priority/.test(token.type)){
_preventIfShould();
_togglePriority();
_disableSelection();
}else if(/org-url/.test(token.type)){
_disableSelection();
_navigateLink();
}else if(/org-image/.test(token.type)){
_disableSelection();
_toggleImageWidget();
}
function _preventIfShould(){
if('ontouchstart' in window) e.preventDefault();
}
function _disableSelection(){
cm.on('beforeSelectionChange', _onSelectionChangeHandler);
function _onSelectionChangeHandler(cm, obj){
obj.update([{
anchor: position,
head: position
}]);
cm.off('beforeSelectionChange', _onSelectionChangeHandler);
}
}
function _foldHeadline(){
var line = position.line;
if(line >= 0){
var cursor = {line: line, ch: 0};
isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor);
}
}
function _toggleCheckbox(){
var line = position.line;
var content = cm.getRange({line: line, ch: token.start}, {line: line, ch: token.end});
var new_content = content === "[X]" || content === "[x]" ? "[ ]" : "[X]";
cm.replaceRange(new_content, {line: line, ch: token.start}, {line: line, ch: token.end});
}
function _toggleTodo(){
var line = position.line;
cm.replaceRange("DONE", {line: line, ch: token.start}, {line: line, ch: token.end});
}
function _toggleDone(){
var line = position.line;
cm.replaceRange("TODO", {line: line, ch: token.start}, {line: line, ch: token.end});
}
function _togglePriority(){
var PRIORITIES = [" [#A] ", " [#B] ", " [#C] ", " [#A] "];
var line = position.line;
var content = cm.getRange({line: line, ch: token.start}, {line: line, ch: token.end});
var new_content = PRIORITIES[PRIORITIES.indexOf(content) + 1];
cm.replaceRange(new_content, {line: line, ch: token.start}, {line: line, ch: token.end});
}
function _toggleImageWidget(){
var exist = !!widgets
.filter(function (line) { return line === position.line; })[0];
if(exist === false){
if(!token.string.match(/\[\[(.*)\]\]/)) return null;
var $node = _buildImage(RegExp.$1);
var widget = cm.addLineWidget(position.line, $node, {coverGutter: false});
widgets.push(position.line);
$node.addEventListener('click', closeWidget);
function closeWidget(){
widget.clear();
$node.removeEventListener('click', closeWidget);
widgets = widgets.filter(function (line) { return line !== position.line; });
}
}
function _buildImage(src){
var $el = document.createElement("div");
var $img = document.createElement("img");
if(/^https?\:\/\//.test(src)){
$img.src = src;
}else{
var root_path = dirname(window.location.pathname.replace(/^\/view/, ''));
var img_path = src;
$img.src = "/api/files/cat?path="+encodeURIComponent(pathBuilder(root_path, img_path));
}
$el.appendChild($img);
return $el;
}
return null;
}
function _navigateLink(){
token.string.match(/\[\[(.*?)\]\[/);
var link = RegExp.$1;
if(!link) return;
if(/^https?\:\/\//.test(link)){
window.open(link);
}else{
var root_path = dirname(window.location.pathname.replace(/^\/view/, ''));
var link_path = link;
window.open("/view"+pathBuilder(root_path, link_path));
}
}
}
CodeMirror.defineMIME("text/org", "org");
function fold(cm, start){
cm.foldCode(start, null, "fold");
}
function unfold(cm, start){
cm.foldCode(start, null, "unfold");
}
function isFold(cm, start){
var line = start.line;
var marks = cm.findMarks(CodeMirror.Pos(line, 0), CodeMirror.Pos(line + 1, 0));
for (var i = 0; i < marks.length; ++i) {
if (marks[i].__isFold && marks[i].find().from.line === line) { return marks[i]; }
}
return false;
}
/*
CodeMirror.afterInit = function(editor){
function fold(cm, start){
cm.foldCode(start, null, "fold");
@ -154,5 +381,662 @@ define([
}
});
};
*/
var org_cycle = function (cm) {
var pos = cm.getCursor();
isFold(cm, pos) ? unfold(cm, pos) : fold(cm, pos);
};
var state = {
stab: 'CONTENT'
};
var org_set_fold = function (cm) {
var cursor = cm.getCursor();
set_folding_mode(cm, state.stab);
cm.setCursor(cursor);
return state.stab;
};
/*
* DONE: Global visibility cycling
* TODO: or move to previous table field.
*/
var org_shifttab = function (cm) {
if(state.stab === "SHOW_ALL"){
state.stab = 'OVERVIEW';
}else if(state.stab === "OVERVIEW"){
state.stab = 'CONTENT';
}else if(state.stab === "CONTENT"){
state.stab = 'SHOW_ALL';
}
set_folding_mode(cm, state.stab);
return state.stab;
};
function set_folding_mode(cm, mode){
if(mode === "OVERVIEW"){
folding_mode_overview(cm);
}else if(mode === "SHOW_ALL"){
folding_mode_all(cm);
}else if(mode === "CONTENT"){
folding_mode_content(cm);
}
cm.refresh();
function folding_mode_overview(cm){
cm.operation(function() {
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){
fold(cm, CodeMirror.Pos(i, 0));
}
});
}
function folding_mode_content(cm){
cm.operation(function() {
var previous_header = null;
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){
fold(cm, CodeMirror.Pos(i, 0));
if(/header/.test(cm.getTokenTypeAt(CodeMirror.Pos(i, 0))) === true){
var level = cm.getLine(i).replace(/^(\*+).*/, "$1").length;
if(previous_header && level > previous_header.level){
unfold(cm, CodeMirror.Pos(previous_header.line, 0));
}
previous_header = {
line: i,
level: level
};
}
}
});
}
function folding_mode_all(cm){
cm.operation(function() {
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){
if(/header/.test(cm.getTokenTypeAt(CodeMirror.Pos(i, 0))) === true){
unfold(cm, CodeMirror.Pos(i, 0));
}
}
});
}
}
/*
* Promote heading or move table column to left.
*/
var org_metaleft = function (cm) {
var line = cm.getCursor().line;
_metaleft(cm, line);
};
function _metaleft(cm, line){
var p = null;
if(p = isTitle(cm, line)){
if(p['level'] > 1) cm.replaceRange('', {line: p.start, ch: 0}, {line: p.start, ch: 1});
}else if(p = isItemList(cm, line)){
for(var i=p.start; i<=p.end; i++){
if(p['level'] > 0) cm.replaceRange('', {line: i, ch: 0}, {line: i, ch: 2});
}
}else if(p = isNumberedList(cm, line)){
for(var i=p.start; i<=p.end; i++){
if(p['level'] > 0) cm.replaceRange('', {line: i, ch: 0}, {line: i, ch: 3});
}
rearrange_list(cm, line);
}
}
/*
* Demote a subtree, a list item or move table column to right.
* In front of a drawer or a block keyword, indent it correctly.
*/
var org_metaright = function (cm){
var line = cm.getCursor().line;
_metaright(cm, line);
};
function _metaright(cm, line) {
var p = null, tmp = null;
if(p = isTitle(cm, line)){
cm.replaceRange('*', {line: p.start, ch: 0});
}else if(p = isItemList(cm, line)){
if(tmp = isItemList(cm, p.start - 1)){
if(p.level < tmp.level + 1){
for(var i=p.start; i<=p.end; i++){
cm.replaceRange(' ', {line: i, ch: 0});
}
}
}
}else if(p = isNumberedList(cm, line)){
if(tmp = isNumberedList(cm, p.start - 1)){
if(p.level < tmp.level + 1){
for(var i=p.start; i<=p.end; i++){
cm.replaceRange(' ', {line: i, ch: 0});
}
rearrange_list(cm, p.start);
}
}
}
}
/*
* Insert a new heading or wrap a region in a table
*/
var org_meta_return = function (cm) {
var line = cm.getCursor().line,
content = cm.getLine(line);
var p = null;
if(p = isItemList(cm, line)){
var level = p.level;
cm.replaceRange('\n'+" ".repeat(level*2)+'- ', {line: p.end, ch: cm.getLine(p.end).length});
cm.setCursor({line: p.end+1, ch: level*2+2});
}else if(p = isNumberedList(cm, line)){
var level = p.level;
cm.replaceRange('\n'+" ".repeat(level*3)+(p.n+1)+'. ', {line: p.end, ch: cm.getLine(p.end).length});
cm.setCursor({line: p.end+1, ch: level*3+3});
rearrange_list(cm, line);
}else if(p = isTitle(cm, line)){
var tmp = previousOfType(cm, 'title', line);
var level = tmp && tmp.level || 1;
cm.replaceRange('\n'+'*'.repeat(level)+' ', {line: line, ch: content.length});
cm.setCursor({line: line+1, ch: level+1});
}else if(content.trim() === ""){
cm.replaceRange('* ', {line: line, ch: 0});
cm.setCursor({line: line, ch: 2});
}else{
cm.replaceRange('\n\n* ', {line: line, ch: content.length});
cm.setCursor({line: line + 2, ch: 2});
}
};
var TODO_CYCLES = ["TODO", "DONE", ""];
/*
* Cycle the thing at point or in the current line, depending on context.
* Depending on context, this does one of the following:
* - TODO: switch a timestamp at point one day into the past
* - DONE: on a headline, switch to the previous TODO keyword.
* - TODO: on an item, switch entire list to the previous bulvar type
* - TODO: on a property line, switch to the previous allowed value
* - TODO: on a clocktable definition line, move time block into the past
*/
var org_shiftleft = function (cm) {
var cycles = [].concat(TODO_CYCLES.slice(0).reverse(), TODO_CYCLES.slice(-1)),
line = cm.getCursor().line,
content = cm.getLine(line),
params = isTitle(cm, line);
if(params === null) return;
params['status'] = cycles[cycles.indexOf(params['status']) + 1];
cm.replaceRange(makeTitle(params), {line: line, ch: 0}, {line: line, ch: content.length});
};
/*
* Cycle the thing at point or in the current line, depending on context.
* Depending on context, this does one of the following:
* - TODO: switch a timestamp at point one day into the future
* - DONE: on a headline, switch to the next TODO keyword.
* - TODO: on an item, switch entire list to the next bulvar type
* - TODO: on a property line, switch to the next allowed value
* - TODO: on a clocktable definition line, move time block into the future
*/
var org_shiftright = function (cm) {
cm.operation(function () {
var cycles = [].concat(TODO_CYCLES, [TODO_CYCLES[0]]),
line = cm.getCursor().line,
content = cm.getLine(line),
params = isTitle(cm, line);
if(params === null) return;
params['status'] = cycles[cycles.indexOf(params['status']) + 1];
cm.replaceRange(makeTitle(params), {line: line, ch: 0}, {line: line, ch: content.length});
});
};
var org_insert_todo_heading = function (cm) {
cm.operation(function () {
var line = cm.getCursor().line,
content = cm.getLine(line);
var p = null;
if(p = isItemList(cm, line)){
var level = p.level;
cm.replaceRange('\n'+" ".repeat(level*2)+'- [ ] ', {line: p.end, ch: cm.getLine(p.end).length});
cm.setCursor({line: line+1, ch: 6+level*2});
}else if(p = isNumberedList(cm, line)){
var level = p.level;
cm.replaceRange('\n'+" ".repeat(level*3)+(p.n+1)+'. [ ] ', {line: p.end, ch: cm.getLine(p.end).length});
cm.setCursor({line: p.end+1, ch: level*3+7});
rearrange_list(cm, line);
}else if(p = isTitle(cm, line)){
var level = p && p.level || 1;
cm.replaceRange('\n'+"*".repeat(level)+' TODO ', {line: line, ch: content.length});
cm.setCursor({line: line+1, ch: level+6});
}else if(content.trim() === ""){
cm.replaceRange('* TODO ', {line: line, ch: 0});
cm.setCursor({line: line, ch: 7});
}else{
cm.replaceRange('\n\n* TODO ', {line: line, ch: content.length});
cm.setCursor({line: line + 2, ch: 7});
}
});
}
/*
* Move subtree up or move table row up.
* Calls org-move-subtree-up or org-table-move-row or
* org-move-item-up, depending on context
*/
var org_metaup = function (cm) {
cm.operation(function () {
var line = cm.getCursor().line;
var p = null;
if(p = isItemList(cm, line)){
var a = isItemList(cm, p.start - 1);
if(a){
swap(cm, [p.start, p.end], [a.start, a.end]);
rearrange_list(cm, line);
}
}else if(p = isNumberedList(cm, line)){
var a = isNumberedList(cm, p.start - 1);
if(a){
swap(cm, [p.start, p.end], [a.start, a.end]);
rearrange_list(cm, line);
}
}else if(p = isTitle(cm, line)){
var _line = line,
a;
do{
_line -= 1;
if(a = isTitle(cm, _line, p.level)){
break;
}
}while(_line > 0);
if(a){
swap(cm, [p.start, p.end], [a.start, a.end]);
org_set_fold(cm);
}
}
});
}
/*
* Move subtree down or move table row down.
* Calls org-move-subtree-down or org-table-move-row or
* org-move-item-down, depending on context
*/
var org_metadown = function (cm) {
cm.operation(function () {
var line = cm.getCursor().line;
var p = null;
if(p = isItemList(cm, line)){
var a = isItemList(cm, p.end + 1);
if(a){
swap(cm, [p.start, p.end], [a.start, a.end]);
}
}else if(p = isNumberedList(cm, line)){
var a = isNumberedList(cm, p.end + 1);
if(a){
swap(cm, [p.start, p.end], [a.start, a.end]);
}
rearrange_list(cm, line);
}else if(p = isTitle(cm, line)){
var a = isTitle(cm, p.end + 1, p.level);
if(a){
swap(cm, [p.start, p.end], [a.start, a.end]);
org_set_fold(cm);
}
}
});
}
var org_shiftmetaright = function(cm) {
cm.operation(function () {
var line = cm.getCursor().line;
var p = null;
if(p = isTitle(cm, line)){
_metaright(cm, line);
for(var i=p.start + 1; i<=p.end; i++){
if(isTitle(cm, i)){
_metaright(cm, i);
}
}
}
});
};
var org_shiftmetaleft = function(cm){
cm.operation(function () {
var line = cm.getCursor().line;
var p = null;
if(p = isTitle(cm, line)){
if(p.level === 1) return;
_metaleft(cm, line);
for(var i=p.start + 1; i<=p.end; i++){
if(isTitle(cm, i)){
_metaleft(cm, i);
}
}
}
});
};
function makeTitle(p){
var content = "*".repeat(p['level'])+" ";
if(p['status']){
content += p['status']+" ";
}
content += p['content'];
return content;
}
function previousOfType(cm, type, line){
var content, tmp, i;
for(i=line - 1; i>0; i--){
if(type === 'list' || type === null){
tmp = isItemList(cm, line);
}else if(type === 'numbered' || type === null){
tmp = isNumberedList(cm, line);
}else if(type === 'title' || type === null){
tmp = isTitle(cm, line);
}
if(tmp !== null){
return tmp;
}
}
return null;
}
function isItemList(cm, line){
var rootLineItem = findRootLine(cm, line);
if(rootLineItem === null) return null;
line = rootLineItem;
var content = cm.getLine(line);
if(content && (content.trimLeft()[0] !== "-" || content.trimLeft()[1] !== " ")) return null;
var padding = content.replace(/^(\s*).*$/, "$1").length;
if(padding % 2 !== 0) return null;
return {
type: 'list',
level: padding / 2,
content: content.trimLeft().replace(/^\s*\-\s(.*)$/, '$1'),
start: line,
end: function(_cm, _line){
var line_candidate = _line,
content = null;
do{
_line += 1;
content = _cm.getLine(_line);
if(content === undefined || content.trimLeft()[0] === "-"){
break;
}else if(/^\s+/.test(content)){
line_candidate = _line;
continue;
}else{
break;
}
}while(_line <= _cm.lineCount())
return line_candidate;
}(cm, line)
};
function findRootLine(_cm, _line){
var content;
do{
content = _cm.getLine(_line);
if(/^\s*\-/.test(content)) return _line;
else if(/^\s+/.test(content) === false){
break;
}
_line -= 1;
}while(_line >= 0);
return null;
}
}
function isNumberedList(cm, line){
var rootLineItem = findRootLine(cm, line);
if(rootLineItem === null) return null;
line = rootLineItem;
var content = cm.getLine(line);
if(/^[0-9]+[\.\)]\s.*$/.test(content && content.trimLeft()) === false) return null;
var padding = content.replace(/^(\s*)[0-9]+.*$/, "$1").length;
if(padding % 3 !== 0) return null;
return {
type: 'numbered',
level: padding / 3,
content: content.trimLeft().replace(/^[0-9]+[\.\)]\s(.*)$/, '$1'),
start: line,
end: function(_cm, _line){
var line_candidate = _line,
content = null;
do{
_line += 1;
content = _cm.getLine(_line);
if(content === undefined || /^[0-9]+[\.\)]/.test(content.trimLeft())){
break;
}else if(/^\s+/.test(content)){
line_candidate = _line;
continue;
}else{
break;
}
}while(_line <= _cm.lineCount())
return line_candidate;
}(cm, line),
// specific
n: parseInt(content.trimLeft().replace(/^([0-9]+).*$/, "$1")),
separator: content.trimLeft().replace(/^[0-9]+([\.\)]).*$/, '$1')
};
function findRootLine(_cm, _line){
var content;
do{
content = _cm.getLine(_line);
if(/^\s*[0-9]+[\.\)]\s/.test(content)) return _line;
else if(/^\s+/.test(content) === false){
break;
}
_line -= 1;
}while(_line >= 0);
return null;
}
}
function isTitle(cm, line, level){
var content = cm.getLine(line);
if(/^\*+\s/.test(content) === false) return null;
var match = content.match(/^(\*+)([\sA-Z]*)\s(.*)$/);
var reference_level = match[1].length;
if(level !== undefined && level !== reference_level){ return null; }
if(match === null) return null;
return {
type: 'title',
level: reference_level,
content: match[3],
start: line,
end: function(_cm, _line){
var line_candidate = _line,
content = null;
do{
_line += 1;
content = _cm.getLine(_line);
if(content === undefined) break;
var match = content.match(/^(\*+)\s.*/);
if(match && match[1] && ( match[1].length === reference_level || match[1].length < reference_level)){
break;
}else{
line_candidate = _line;
continue;
}
}while(_line <= _cm.lineCount())
return line_candidate;
}(cm, line),
// specific
status: match[2].trim()
};
}
function rearrange_list(cm, line){
var line_inferior = find_limit_inferior(cm, line);
var line_superior = find_limit_superior(cm, line);
var last_p = null, p;
for(var i=line_inferior; i<=line_superior; i++){
if(p = isNumberedList(cm, i)){
// rearrange numbers on the numbered list
if(last_p){
if(p.level === last_p.level){
var tmp = findLastAtLevel(cm, p.start, line_inferior, p.level);
if(tmp && p.n !== tmp.n + 1) setNumber(cm, p.start, tmp.n + 1);
}else if(p.level > last_p.level){
if(p.n !== 1){
setNumber(cm, p.start, 1);
}
}else if(p.level < last_p.level){
var tmp = findLastAtLevel(cm, p.start, line_inferior, p.level);
if(tmp && p.n !== tmp.n + 1) setNumber(cm, p.start, tmp.n + 1);
}
}else{
if(p.n !== 1){ setNumber(cm, p.start, 1); }
}
}
if(p = (isNumberedList(cm, i) || isItemList(cm, i))){
// rearrange spacing levels in list
if(last_p){
if(p.level > last_p.level){
if(p.level !== last_p.level + 1){
setLevel(cm, [p.start, p.end], last_p.level + 1, p.type);
}
}
}else{
if(p.level !== 0){
setLevel(cm, [p.start, p.end], 0, p.type);
}
}
}
last_p = p;
// we can process content block instead of line
if(p){
i += (p.end - p.start);
}
}
function findLastAtLevel(_cm, line, line_limit_inf, level){
var p;
do{
line -= 1;
if((p = isNumberedList(_cm, line)) && p.level === level)
return p;
}while(line > line_limit_inf);
return null;
}
function setLevel(_cm, range, level, type){
var content, i;
for(i=range[0]; i<=range[1]; i++){
content = cm.getLine(i).trimLeft();
var n_spaces = function(_level, _line, _type){
var spaces = _level * 3;
if(_line > 0){
spaces += _type === 'numbered' ? 3 : 2;
}
return spaces;
}(level, i - range[0], type)
content = " ".repeat(n_spaces) + content;
cm.replaceRange(content, {line: i, ch: 0}, {line: i, ch: _cm.getLine(i).length});
}
}
function setNumber(_cm, line, level){
var content = _cm.getLine(line);
var new_content = content.replace(/[0-9]+\./, level+".");
cm.replaceRange(new_content, {line: line, ch: 0}, {line: line, ch: content.length});
}
function find_limit_inferior(_cm, _line){
var content, p, match, line_candidate = _line;
do{
content = _cm.getLine(_line);
p = isNumberedList(_cm, _line);
match = /(\s+).*$/.exec(content);
if(p){ line_candidate = _line;}
if(!p || !match) break;
_line -= 1;
}while(_line >= 0);
return line_candidate;
}
function find_limit_superior(_cm, _line){
var content, p, match, line_candidate = _line;
do{
content = _cm.getLine(_line);
p = isNumberedList(_cm, _line);
match = /(\s+).*$/.exec(content);
if(p){ line_candidate = _line;}
if(!p || !match) break;
_line += 1;
}while(_line < _cm.lineCount());
return line_candidate;
}
}
function swap(cm, from, to){
var from_content = cm.getRange({line: from[0], ch: 0}, {line: from[1], ch: cm.getLine(from[1]).length}),
to_content = cm.getRange({line: to[0], ch: 0}, {line: to[1], ch: cm.getLine(to[1]).length}),
cursor = cm.getCursor();
if(to[0] > from[0]){
// moving down
cm.replaceRange(
from_content,
{line: to[0], ch:0},
{line: to[1], ch: cm.getLine(to[1]).length}
);
cm.replaceRange(
to_content,
{line: from[0], ch:0},
{line: from[1], ch: cm.getLine(from[1]).length}
);
cm.setCursor({
line: cursor.line + (to[1] - to[0] + 1),
ch: cursor.ch
});
}else{
// moving up
cm.replaceRange(
to_content,
{line: from[0], ch:0},
{line: from[1], ch: cm.getLine(from[1]).length}
);
cm.replaceRange(
from_content,
{line: to[0], ch:0},
{line: to[1], ch: cm.getLine(to[1]).length}
);
cm.setCursor({
line: cursor.line - (to[1] - to[0] + 1),
ch: cursor.ch
});
}
}
});

View File

@ -99,7 +99,7 @@ define([
if (lessEngine) {
cb(lessEngine);
} else {
require(['/bower_components/less/dist/less.min.js'], function (Less) {
require(['/common/less.min.js'], function (Less) {
if (lessEngine) { return void cb(lessEngine); }
lessEngine = Less;
Less.functions.functionRegistry.add('LessLoader_currentFile', function () {

View File

@ -12,7 +12,7 @@ define(function() {
* You should never remove the drive from this list.
*/
config.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard',
'meet', /*'oodoc', 'ooslide',*/ 'file', 'todo', 'contacts'];
'meet', /*'oodoc', 'ooslide',*/ 'file', /* 'todo', */ 'contacts'];
/* The registered only types are apps restricted to registered users.
* You should never remove apps from this list unless you know what you're doing. The apps
* listed here by default can't work without a user account.
@ -20,7 +20,7 @@ define(function() {
* users and these users will be redirected to the login page if they still try to access
* the app
*/
config.registeredOnlyTypes = ['file', 'contacts', 'oodoc', 'ooslide', 'notifications'];
config.registeredOnlyTypes = ['file', 'contacts', 'oodoc', 'ooslide', 'notifications', 'support'];
/* CryptPad is available is multiple languages, but only English and French are maintained
* by the developers. The other languages may be outdated, and any missing string for a langauge
@ -40,6 +40,12 @@ define(function() {
// config.imprint = true;
// config.imprint = 'https://xwiki.com/en/company/legal-notice';
/* You can display a link to your own privacy policy in the static pages footer.
* To do so, set the following value to the absolute URL of your privacy policy.
*/
config.privacy = '/privacy.html';
// config.privacy = 'https://xwiki.com/en/company/PrivacyPolicy';
/* Cryptpad apps use a common API to display notifications to users
* by default, notifications are hidden after 5 seconds
* You can change their duration here (measured in milliseconds)

View File

@ -1,5 +1,5 @@
(function (window) {
var factory = function (Util, Crypto, Nacl) {
var factory = function (Util, Crypto, Keys, Nacl) {
var Hash = window.CryptPad_Hash = {};
var uint8ArrayToHex = Util.uint8ArrayToHex;
@ -28,6 +28,13 @@ var factory = function (Util, Crypto, Nacl) {
};
};
Hash.getSignPublicFromPrivate = function (edPrivateSafeStr) {
var edPrivateStr = Crypto.b64AddSlashes(edPrivateSafeStr);
var privateKey = Nacl.util.decodeBase64(edPrivateStr);
var keyPair = Nacl.sign.keyPair.fromSecretKey(privateKey);
return Nacl.util.encodeBase64(keyPair.publicKey);
};
var getEditHashFromKeys = Hash.getEditHashFromKeys = function (secret) {
var version = secret.version;
var data = secret.keys;
@ -92,9 +99,7 @@ var factory = function (Util, Crypto, Nacl) {
}
};
Hash.getUserHrefFromKeys = function (origin, username, pubkey) {
return origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-');
};
Hash.getPublicSigningKeyString = Keys.serialize;
var fixDuplicateSlashes = function (s) {
return s.replace(/\/+/g, '/');
@ -568,14 +573,20 @@ Version 1
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(require("./common-util"), require("chainpad-crypto"), require("tweetnacl/nacl-fast"));
module.exports = factory(
require("./common-util"),
require("chainpad-crypto"),
require("./common-signing-keys"),
require("tweetnacl/nacl-fast")
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/bower_components/chainpad-crypto/crypto.js',
'/common/common-signing-keys.js',
'/bower_components/tweetnacl/nacl-fast.min.js'
], function (Util, Crypto) {
return factory(Util, Crypto, window.nacl);
], function (Util, Crypto, Keys) {
return factory(Util, Crypto, Keys, window.nacl);
});
} else {
// unsupported initialization

View File

@ -145,12 +145,12 @@ define([
};
dialog.okButton = function (content, classString) {
var sel = typeof(classString) === 'string'? 'button.ok.' + classString:'button.ok.primary';
var sel = typeof(classString) === 'string'? 'button.ok.' + classString:'button.btn.ok.primary';
return h(sel, { tabindex: '2', }, content || Messages.okButton);
};
dialog.cancelButton = function (content, classString) {
var sel = typeof(classString) === 'string'? 'button.' + classString:'button.cancel';
var sel = typeof(classString) === 'string'? 'button.' + classString:'button.btn.cancel';
return h(sel, { tabindex: '1'}, content || Messages.cancelButton);
};
@ -196,6 +196,7 @@ define([
]);
var $frame = $(frame);
frame.closeModal = function (cb) {
frame.closeModal = function () {}; // Prevent further calls
$frame.fadeOut(150, function () {
$frame.detach();
if (typeof(cb) === "function") { cb(); }
@ -327,11 +328,7 @@ define([
var input = dialog.textInput();
var tagger = dialog.frame([
dialog.message([
Messages.tags_add,
h('br'),
Messages.tags_searchHint,
]),
dialog.message([ Messages.tags_add ]),
input,
h('center', h('small', Messages.tags_notShared)),
dialog.nav(),
@ -359,11 +356,15 @@ define([
var $cancel = findCancelButton(tagger).click(function (e) {
close(null, e);
});
listener = listenForKeys(function () {
$ok.click();
}, function () {
$(tagger).on('keydown', function (e) {
if (e.which === 27) {
$cancel.click();
}, tagger);
return;
}
if (e.which === 13) {
$ok.click();
}
});
$(tagger).on('click submit', function (e) {
e.stopPropagation();
@ -376,6 +377,14 @@ define([
field.focus();
});
var $field = field.tokenfield.closest('.tokenfield').find('.token-input');
$field.on('keypress', function (e) {
if (!$field.val() && e.which === 13) { return void $ok.click(); }
});
$field.on('keydown', function (e) {
if (!$field.val() && e.which === 27) { return void $cancel.click(); }
});
return tagger;
};
@ -386,7 +395,11 @@ define([
var navs = [];
buttons.forEach(function (b) {
if (!b.name || !b.onClick) { return; }
var button = h('button', { tabindex: '1', 'class': b.className || '' }, b.name);
var button = h('button', { tabindex: '1', 'class': b.className || '' }, [
b.iconClass ? h('i' + b.iconClass) : undefined,
b.name
]);
button.classList.add('btn');
var todo = function () {
var noClose = b.onClick();
if (noClose) { return; }
@ -464,16 +477,23 @@ define([
UI.createModal = function (cfg) {
var $body = cfg.$body || $('body');
var $blockContainer = $body.find('#'+cfg.id);
if (!$blockContainer.length) {
$blockContainer = $(h('div.cp-modal-container#'+cfg.id, {
var $blockContainer = cfg.id && $body.find('#'+cfg.id);
if (!$blockContainer || !$blockContainer.length) {
var id = '';
if (cfg.id) { id = '#'+cfg.id; }
$blockContainer = $(h('div.cp-modal-container'+id, {
tabindex: 1
}));
}
var deleted = false;
var hide = function () {
if (cfg.onClose) { return void cfg.onClose(); }
if (deleted) { return; }
$blockContainer.hide();
if (cfg.onClosed) { cfg.onClosed(); }
if (!cfg.id) {
deleted = true;
$blockContainer.remove();
}
if (cfg.onClose) { cfg.onClose(); }
};
$blockContainer.html('').appendTo($body);
var $block = $(h('div.cp-modal')).appendTo($blockContainer);
@ -630,9 +650,6 @@ define([
var $ok = $(ok).click(function (ev) { close(true, ev); });
var $cancel = $(cancel).click(function (ev) { close(false, ev); });
if (opt.cancelClass) { $cancel.addClass(opt.cancelClass); }
if (opt.okClass) { $ok.addClass(opt.okClass); }
listener = listenForKeys(function () {
$ok.click();
}, function () {
@ -791,6 +808,7 @@ define([
UI.createHelper = function (href, text) {
var q = h('a.fa.fa-question-circle', {
'data-cptippy-html': true,
style: 'text-decoration: none !important;',
title: text,
href: href,
@ -1028,7 +1046,7 @@ define([
}
},
//arrowType: 'round',
dynamicTitle: true,
dynamicTitle: false,
arrowTransform: 'scale(2)',
zIndex: 100000001
});
@ -1036,6 +1054,7 @@ define([
var MutationObserver = window.MutationObserver;
var addTippy = function (i, el) {
if (el._tippy) { return; }
if (!el.getAttribute('title')) { return; }
if (el.nodeName === 'IFRAME') { return; }
var opts = {
distance: 15
@ -1045,6 +1064,10 @@ define([
}).forEach(function (obj) {
opts[obj.name.slice(11)] = obj.value;
});
if (!el.getAttribute('data-cptippy-html') && !el.fixHTML) {
el.setAttribute('title', Util.fixHTML(el.getAttribute('title'))); // fixHTML
el.fixHTML = true; // Don't clean HTML twice on the same element
}
Tippy(el, opts);
};
// This is the robust solution to remove dangling tooltips
@ -1067,6 +1090,7 @@ define([
}
}
if (mutation.type === "attributes" && mutation.attributeName === "title") {
mutation.target.fixHTML = false;
addTippy(0, mutation.target);
}
});
@ -1350,5 +1374,43 @@ define([
};
};
/* Given two jquery objects (a 'button' and a 'drawer')
add handlers to make it such that clicking the button
displays the drawer contents, and blurring the button
hides the drawer content. Used for toolbar buttons at the moment.
*/
UI.createDrawer = function ($button, $content) {
$button.click(function () {
$content.toggle();
$button.removeClass('cp-toolbar-button-active');
if ($content.is(':visible')) {
$button.addClass('cp-toolbar-button-active');
$content.focus();
var wh = $(window).height();
var topPos = $button[0].getBoundingClientRect().bottom;
$content.css('max-height', Math.floor(wh - topPos - 1)+'px');
}
});
var onBlur = function (e) {
if (e.relatedTarget) {
var $relatedTarget = $(e.relatedTarget);
if ($relatedTarget.is('.cp-toolbar-drawer-button')) { return; }
if ($relatedTarget.parents('.cp-toolbar-drawer-content').length) {
$relatedTarget.blur(onBlur);
return;
}
}
$button.removeClass('cp-toolbar-button-active');
$content.hide();
};
$content.blur(onBlur).appendTo($button);
$('body').keydown(function (e) {
if (e.which === 27) {
$content.blur();
}
});
};
return UI;
});

View File

@ -0,0 +1,89 @@
(function () {
var factory = function () {
var Keys = {};
/* Parse the new format of "Signing Public Keys".
If anything about the input is found to be invalid, return;
this will fall back to the old parsing method
*/
var parseNewUser = function (userString) {
if (!/^\[.*?@.*\]$/.test(userString)) { return; }
var temp = userString.slice(1, -1);
var domain, username, pubkey;
temp = temp
.replace(/\/([a-zA-Z0-9+-]{43}=)$/, function (all, k) {
pubkey = k.replace(/-/g, '/');
return '';
});
if (!pubkey) { return; }
var index = temp.lastIndexOf('@');
if (index < 1) { return; }
domain = temp.slice(index + 1);
username = temp.slice(0, index);
return {
domain: domain,
user: username,
pubkey: pubkey
};
};
var isValidUser = function (parsed) {
if (!parsed) { return; }
if (!(parsed.domain && parsed.user && parsed.pubkey)) { return; }
return true;
};
Keys.parseUser = function (user) {
var parsed = parseNewUser(user);
if (isValidUser(parsed)) { return parsed; }
var domain, username, pubkey;
user.replace(/^https*:\/\/([^\/]+)\/user\/#\/1\/([^\/]+)\/([a-zA-Z0-9+-]{43}=)$/,
function (a, d, u, k) {
domain = d;
username = u;
pubkey = k.replace(/-/g, '/');
return '';
});
if (!domain) { throw new Error("Could not parse user id [" + user + "]"); }
return {
domain: domain,
user: username,
pubkey: pubkey
};
};
/*
0. usernames may contain spaces or many other wacky characters, so enclose the whole thing in square braces so we know its boundaries. If the formatted string does not include these we know it is either a _v1 public key string_ or _an incomplete string_. Start parsing by removing them.
1. public keys should have a fixed length, so slice them off of the end of the string.
2. domains cannot include `@`, so find the last occurence of it in the signing key and slice everything thereafter.
3. the username is everything before the `@`.
*/
Keys.serialize = function (origin, username, pubkey) {
return '[' +
username +
'@' +
origin.replace(/https*:\/\//, '') +
'/' +
pubkey.replace(/\//g, '-') +
']';
// return origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-');
};
return Keys;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory();
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([], factory);
}
}());

View File

@ -110,8 +110,11 @@ define([
c2.height = D.dim;
var ctx = c2.getContext('2d');
try {
ctx.drawImage(canvas, D.x, D.y, D.w, D.h);
} catch (e) {
return void cb('ERROR');
}
cb(void 0, c2.toDataURL());
};
@ -157,6 +160,8 @@ define([
viewport: page.getViewport(scale)
}).promise.then(function () {
return canvas;
}).catch(function () {
cb('ERROR');
});
};
PDFJS.getDocument(url).promise
@ -190,7 +195,14 @@ define([
});
reader.readAsText(blob);
};
Thumb.fromBlob = function (blob, cb) {
Thumb.fromBlob = function (blob, _cb) {
var cb = Util.once(_cb);
// The blob is already in memory, it should be super-fast to make a thumbnail
// ==> 1s timeout
setTimeout(function () {
cb('TIMEOUT');
}, 1000);
try {
if (blob.type.indexOf('video/') !== -1) {
return void Thumb.fromVideoBlob(blob, cb);
}
@ -200,7 +212,13 @@ define([
if (Util.isPlainTextFile(blob.type, blob.name)) {
return void Thumb.fromPlainTextBlob(blob, cb);
}
Thumb.fromImageBlob(blob, cb);
if (blob.type.indexOf('image/') !== -1) {
return void Thumb.fromImageBlob(blob, cb);
}
} catch (e) {
return void cb('THUMBNAIL_ERROR');
}
return void cb('NO_THUMBNAIL');
};
window.html2canvas = undefined;

File diff suppressed because it is too large Load Diff

View File

@ -310,6 +310,10 @@
clearTimeout(to);
to = setTimeout(Util.bake(f, Util.slice(arguments)), ms);
};
g.clear = function () {
clearTimeout(to);
to = undefined;
};
return g;
};
@ -347,10 +351,6 @@
};
};
Util.slice = function (A) {
return Array.prototype.slice.call(A);
};
Util.blobToImage = function (blob, cb) {
var reader = new FileReader();
reader.onloadend = function() {

View File

@ -76,6 +76,9 @@ define([
finish(Session, void 0, rt.getUserDoc());
};
config.onError = function (info) {
finish(Session, info.error);
};
config.onChannelError = function (info) {
finish(Session, info.error);
};

View File

@ -63,7 +63,10 @@ define([
common.setLanguage = function (l, cb) {
var LS_LANG = "CRYPTPAD_LANG";
localStorage.setItem(LS_LANG, l);
cb();
postMessage("SET_ATTRIBUTE", {
attr: ['general', 'language'],
value: l
}, cb);
};
common.makeNetwork = function (cb) {
@ -80,6 +83,40 @@ define([
});
};
common.getTeamsId = function () {
postMessage("GET", {
key: ['teams'],
}, function (obj) {
if (obj.error) { return; }
Object.keys(obj || {}).forEach(function (id) {
console.log(obj[id].metadata.name, ':', id);
});
});
};
common.fixFork = function (teamId) {
var i = 0;
var send = function () {
if (i >= 110) {
postMessage("SET", {
teamId: teamId,
key: ['fixFork'],
}, function () {});
return;
}
postMessage("SET", {
teamId: teamId,
key: ['fixFork'],
value: i
}, function () {
i++;
setTimeout(send, 500);
});
};
send();
};
// RESTRICTED
// Settings only
common.resetDrive = function (cb) {
@ -595,7 +632,12 @@ define([
}
}).nThen(function () {
Crypt.get(parsed.hash, function (err, val) {
if (err) { throw new Error(err); }
if (err) {
return void cb(err);
}
if (!val) {
return void cb('ENOENT');
}
try {
// Try to fix the title before importing the template
var parsed = JSON.parse(val);
@ -628,14 +670,14 @@ define([
Crypt.get(parsed.hash, _waitFor(function (err, _val) {
if (err) {
_waitFor.abort();
return void cb();
return void cb(err);
}
try {
val = JSON.parse(_val);
fixPadMetadata(val, true);
} catch (e) {
_waitFor.abort();
return void cb();
return void cb(e.message);
}
}), optsGet);
return;
@ -654,7 +696,7 @@ define([
Util.fetch(src, waitFor(function (err, _u8) {
if (err) {
_waitFor.abort();
return void waitFor.abort();
return void cb(err);
}
u8 = _u8;
}));
@ -663,7 +705,7 @@ define([
FileCrypto.decrypt(u8, key, waitFor(function (err, _res) {
if (err || !_res.content) {
_waitFor.abort();
return void waitFor.abort();
return void cb(err);
}
res = _res;
}));
@ -1567,6 +1609,26 @@ define([
var allocated = Login.allocateBytes(bytes);
blockKeys = allocated.blockKeys;
}));
}).nThen(function (waitFor) {
var blockUrl = Block.getBlockUrl(blockKeys);
// Check whether there is a block at that location
Util.fetch(blockUrl, waitFor(function (err, block) {
// If there is no block or the block is invalid, continue.
if (err) {
console.log("no block found");
return;
}
var decryptedBlock = Block.decrypt(block, blockKeys);
if (!decryptedBlock) {
console.error("Found a login block but failed to decrypt");
return;
}
// If there is already a valid block, abort! We risk overriding another user's data
waitFor.abort();
cb({ error: 'EEXISTS' });
}));
}).nThen(function (waitFor) {
// Write the new login block
var temp = {
@ -1708,14 +1770,17 @@ define([
var ver = arr[1];
if (!ver) { return; }
var verArr = ver.split('.');
verArr[2] = 0;
//verArr[2] = 0;
if (verArr.length !== 3) { return; }
var stored = currentVersion || '0.0.0';
var storedArr = stored.split('.');
storedArr[2] = 0;
//storedArr[2] = 0;
var shouldUpdate = JSON.stringify(verArr) !== JSON.stringify(storedArr);
/*
var shouldUpdate = parseInt(verArr[0]) !== parseInt(storedArr[0]) ||
(parseInt(verArr[0]) === parseInt(storedArr[0]) &&
parseInt(verArr[1]) !== parseInt(storedArr[1]));
*/
if (!shouldUpdate) { return; }
currentVersion = ver;
localStorage[CRYPTPAD_VERSION] = ver;
@ -1926,7 +1991,7 @@ define([
}).nThen(function (waitFor) {
var blockHash = LocalStore.getBlockHash();
if (blockHash) {
console.log(blockHash);
console.debug("Block hash is present");
var parsed = Block.parseBlockHash(blockHash);
if (typeof(parsed) !== 'object') {

View File

@ -3,7 +3,7 @@ define([], function () {
var indexOfNode = tree.indexOfNode = function (el) {
if (!(el && el.parentNode)) {
console.log("No parentNode found!");
console.log("No parentNode found!", el);
throw new Error('No parentNode found!');
}
return Array.prototype.indexOf.call(el.parentNode.childNodes, el);
@ -26,6 +26,7 @@ define([], function () {
leaf nodes of a tree
*/
var rightmostNode = tree.rightmostNode = function (el) {
if (!el) { return null; }
var childNodeCount = childCount(el);
if (!childNodeCount) { // no children
return el; // return the element
@ -53,7 +54,7 @@ define([], function () {
if (!el.parentNode) { return null; }
if (i === 0) {
if (root && el.parentNode === root.childNodes[0]) { return null; }
return rightmostNode(previousNode(el.parentNode));
return rightmostNode(previousNode(el.parentNode, root));
} else {
return rightmostNode(el.parentNode.childNodes[i-1]);
}
@ -77,6 +78,10 @@ define([], function () {
// a and b might be the same element
if (a === b) { return 0; }
// If we're selecting an entire node containing a single text node,
// b can be the child of a. Order is correct.
if (b.parentNode && b.parentNode === a) { return 1; }
var cur = b;
while (cur) {
cur = previousNode(cur, root);

View File

@ -19,13 +19,119 @@ define([
var renderer = new Marked.Renderer();
var restrictedRenderer = new Marked.Renderer();
var pluginLoaded = Util.mkEvent();
DiffMd.onPluginLoaded = pluginLoaded.reg;
var mermaidThemeCSS = //".node rect { fill: #DDD; stroke: #AAA; } " +
"rect.task, rect.task0, rect.task2 { stroke-width: 1 !important; rx: 0 !important; } " +
"g.grid g.tick line { opacity: 0.25; }" +
"g.today line { stroke: red; stroke-width: 1; stroke-dasharray: 3; opacity: 0.5; }";
var Mermaid = {
init: function () {}
__stubbed: true,
init: function () {
require([
'mermaid',
'css!/code/mermaid-new.css'
], function (_Mermaid) {
console.debug("loaded mermaid");
if (Mermaid.__stubbed) {
Mermaid = _Mermaid;
Mermaid.initialize({
gantt: { axisFormat: '%m-%d', },
"themeCSS": mermaidThemeCSS,
});
}
pluginLoaded.fire();
});
}
};
require(['mermaid', 'css!/code/mermaid-new.css'], function (_Mermaid) {
Mermaid = _Mermaid;
var Mathjax = {
__stubbed: true,
tex2svg: function (a, b) {
require([
'/bower_components/MathJax/es5/tex-svg.js',
], function () {
console.debug("Loaded mathjax");
if (Mathjax.__stubbed) {
Mathjax = window.MathJax;
}
Mathjax.tex2svg(a, b);
pluginLoaded.fire();
});
}
};
var drawMarkmap;
var MarkMapTransform;
var Markmap;
var markmapLoaded = false;
var loadMarkmap = function ($el) {
require([
'/lib/markmap/transform.min.js',
'/lib/markmap/view.min.js',
], function (_Transform, _View) {
if (!markmapLoaded) {
console.debug("Loaded markmap");
MarkMapTransform = _Transform;
Markmap = _View;
markmapLoaded = true;
}
drawMarkmap($el);
pluginLoaded.fire();
});
};
var sfCommon;
var fixMarkmapClickables = function ($svg) {
// find all links in the tree and do the following for each one
var onClick = function (e) {
e.preventDefault();
e.stopImmediatePropagation();
var $el = $(e.target);
// Open links only from the preview modal
if (!sfCommon) { return void console.error('No sfCommon'); }
var href = $el.attr('href');
if (!href || !/^(https?:\/\/|\/)/.test(href)) { return; }
if (/^http/.test(href)) {
sfCommon.openUnsafeURL(href);
return;
}
sfCommon.openURL(href);
};
$svg.find('a').click(onClick);
// make sure the links added later by collapsing/expading the map are also safe
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
var n;
for (var i = 0; i < mutation.addedNodes.length; i++) {
n = mutation.addedNodes[i];
if (n.nodeName === "A") { return void n.addEventListener('click', onClick); }
$(n).find('a').click(onClick);
}
}
});
});
observer.observe($svg[0], {
childList: true,
subtree: true
});
};
drawMarkmap = function ($el) {
if (!markmapLoaded) { return void loadMarkmap($el); }
if (!$el) { return console.error("no element provided"); }
var data = MarkMapTransform.transform($el[0].getAttribute("markmap-source"));
$el[0].innerHTML = "<svg width='100%' height='600'/>";
Markmap.markmap($el[0].firstChild, data);
fixMarkmapClickables($el);
};
var highlighter = function () {
return function(code, lang) {
@ -83,9 +189,15 @@ define([
var mediaMap = {};
var defaultCode = renderer.code;
renderer.code = function (code, language) {
if (!code || typeof(code) !== 'string' || !code.trim()) { return defaultCode.apply(renderer, arguments); }
if (language === 'mermaid' && code.match(/^(graph|pie|gantt|sequenceDiagram|classDiagram|gitGraph)/)) {
return '<pre class="mermaid">'+Util.fixHTML(code)+'</pre>';
return '<pre class="mermaid" data-plugin="mermaid">'+Util.fixHTML(code)+'</pre>';
} else if (language === 'markmap') {
return '<pre class="markmap" data-plugin="markmap">'+Util.fixHTML(code)+'</pre>';
} else if (language === 'mathjax') {
return '<pre class="mathjax" data-plugin="mathjax">'+Util.fixHTML(code)+'</pre>';
} else {
return defaultCode.apply(renderer, arguments);
}
@ -190,7 +302,6 @@ define([
return renderParagraph(p);
};
var MutationObserver = window.MutationObserver;
var forbiddenTags = [
'SCRIPT',
'IFRAME',
@ -288,6 +399,8 @@ define([
return patch;
};
var plugins = {};
var removeMermaidClickables = function ($el) {
// find all links in the tree and do the following for each one
$el.find('a').each(function (index, a) {
@ -305,7 +418,82 @@ define([
$el.find('.clickable').removeClass('clickable');
};
plugins.mermaid = {
name: 'mermaid',
attr: 'mermaid-source',
render: function ($el) {
Mermaid.init(undefined, $el);
// clickable elements in mermaid don't work well with our sandboxing setup
// the function below strips clickable elements but still leaves behind some artifacts
// tippy tooltips might still be useful, so they're not removed. It would be
// preferable to just support links, but this covers up a rough edge in the meantime
removeMermaidClickables($el);
}
};
plugins.markmap = {
name: 'markmap',
attr: 'markmap-source',
render: function ($el) {
drawMarkmap($el);
}
};
plugins.mathjax = {
name: 'mathjax',
attr: 'mathjax-source',
render: function renderMathjax ($el) {
var el = $el[0];
if (!el) { return; }
var code = el.getAttribute("mathjax-source");
var svg = Mathjax.tex2svg(code, {display: true});
if (!svg) { return; }
svg.innerHTML = svg.innerHTML.replace(/xlink:href/g, "href");
var wrapper = document.createElement('span');
wrapper.innerHTML = svg.innerHTML;
el.innerHTML = wrapper.outerHTML;
}
};
var getAvailableCachedElement = function ($content, cache, src) {
var cached = cache[src];
if (!Array.isArray(cached)) { return; }
var root = $content[0];
var l = cached.length;
for (var i = 0; i < l; i++) {
if (!root.contains(cached[i])) {
return cached[i];
}
}
};
var cacheRenderedElement = function (cache, src, el) {
if (Array.isArray(cache[src])) {
cache[src].push(el);
} else {
cache[src] = [ el ];
}
};
// remove elements from the cache that are not embedded in the dom
var clearUnusedCacheEntries = function ($content, plugins) {
var root = $content[0];
Object.keys(plugins).forEach(function (name) {
var plugin = plugins[name];
var cache = plugin.cache;
Object.keys(cache).forEach(function (key) {
var list = cache[key];
if (!Array.isArray(list)) { return; }
cache[key] = list.filter(function (el) {
return root.contains(el);
});
});
});
};
DiffMd.apply = function (newHtml, $content, common) {
if (!sfCommon) { sfCommon = common; }
var contextMenu = common.importMediaTagMenu();
var id = $content.attr('id');
if (!id) { throw new Error("The element must have a valid id"); }
@ -325,8 +513,10 @@ define([
var Dom = domFromHTML($('<div>').append($div).html());
$content[0].normalize();
var mermaid_source = [];
var mermaid_cache = {};
Object.keys(plugins).forEach(function (id) {
plugins[id].source = [];
plugins[id].cache = {};
});
var canonicalizeMermaidSource = function (src) {
// ignore changes to empty lines, since that won't affect
@ -334,12 +524,15 @@ define([
return src.replace(/\n[ \t]*\n*[ \t]*\n/g, '\n');
};
// iterate over the unrendered mermaid inputs, caching their source as you go
$(newDomFixed).find('pre.mermaid').each(function (index, el) {
// iterate over the unrendered mermaid and markmap inputs,
// caching their source as you go
$(newDomFixed).find('pre[data-plugin]').each(function (index, el) {
if (el.childNodes.length === 1 && el.childNodes[0].nodeType === 3) {
var plugin = plugins[el.getAttribute('data-plugin')];
if (!plugin) { return; }
var src = canonicalizeMermaidSource(el.childNodes[0].wholeText);
el.setAttribute('mermaid-source', src);
mermaid_source[index] = src;
el.setAttribute(plugin.attr, src);
plugin.source[index] = src;
}
});
@ -347,12 +540,21 @@ define([
var $parent = $content.parent();
var scrollTop = $parent.scrollTop();
// iterate over rendered mermaid charts
$content.find('pre.mermaid:not([processed="true"])').each(function (index, el) {
$content.find('pre[data-plugin]:not([processed="true"])').each(function (index, el) {
var plugin = plugins[el.getAttribute('data-plugin')];
if (!plugin) { return; }
// retrieve the attached source code which it was drawn
var src = el.getAttribute('mermaid-source');
var src = el.getAttribute(plugin.attr);
/* The new source might have syntax errors that will prevent rendering.
It might be preferable to keep the existing state instead of removing it
if you don't have something better to display. Ideally we should display
the cause of the syntax error so that the user knows what to correct. */
//if (plugin.name === "mermaid" && !Mermaid.parse(src)) { } // TODO
// check if that source exists in the set of charts which are about to be rendered
if (mermaid_source.indexOf(src) === -1) {
if (plugin.source.indexOf(src) === -1) {
// if it's not, then you can remove it
if (el.parentNode && el.parentNode.children.length) {
el.parentNode.removeChild(el);
@ -360,20 +562,32 @@ define([
} else if (el.childNodes.length === 1 && el.childNodes[0].nodeType !== 3) {
// otherwise, confirm that the content of the rendered chart is not a text node
// and keep a copy of it
mermaid_cache[src] = el.childNodes[0];
cacheRenderedElement(plugin.cache, src, el.childNodes[0]);
}
});
var oldDom = domFromHTML($content[0].outerHTML);
var MutationObserver = window.MutationObserver;
var onPreview = function ($mt) {
return function () {
var isSvg = $mt.is('pre.mermaid');
if (window.event.ctrlKey) { return; }
var mts = [];
$content.find('media-tag, pre.mermaid').each(function (i, el) {
// Get all previewable elements from the doc
$content.find('media-tag, pre[data-plugin]').each(function (i, el) {
if (el.nodeName.toLowerCase() === "pre") {
var clone = el.cloneNode();
var plugin = plugins[el.getAttribute('data-plugin')];
if (!plugin) { return; }
return void mts.push({
svg: el.cloneNode(true)
svg: clone,
render: function () {
var $el = $(clone);
$el.text(clone.getAttribute(plugin.attr));
$el.attr('data-processed', '');
plugin.render($el);
}
});
}
var $el = $(el);
@ -384,9 +598,16 @@ define([
});
// Find initial position
// If the element is supported by one of our plugin types
// (mermaid, mathjax, or markmap) get the corresponding attribute
var isSvg = $mt.is('pre[data-plugin]');
var plugin = isSvg && plugins[$mt.attr('data-plugin')];
// Get initial idx
var idx = -1;
mts.some(function (obj, i) {
if (isSvg && $mt.find('svg').attr('id') === $(obj.svg).find('svg').attr('id')) {
if (isSvg && $mt.attr(plugin.attr) === $(obj.svg).attr(plugin.attr)) {
idx = i;
return true;
}
@ -395,10 +616,18 @@ define([
return true;
}
});
// Not found, re-render
if (idx === -1) {
if (isSvg) {
if (isSvg && $mt.attr(plugin.attr)) {
var clone = $mt[0].cloneNode();
mts.unshift({
svg: $mt[0].cloneNode(true)
svg: clone,
render: function () {
var $el = $(clone);
$el.text(clone.getAttribute(plugin.attr));
$el.attr('data-processed', '');
plugin.render($el);
}
});
} else {
mts.unshift({
@ -409,7 +638,9 @@ define([
idx = 0;
}
setTimeout(function () {
common.getMediaTagPreview(mts, idx);
});
};
};
@ -418,7 +649,7 @@ define([
throw new Error(patch);
} else {
DD.apply($content[0], patch);
var $mts = $content.find('media-tag:not(:has(*))');
var $mts = $content.find('media-tag');
$mts.each(function (i, el) {
var $mt = $(el).contextmenu(function (e) {
e.preventDefault();
@ -426,6 +657,16 @@ define([
$(contextMenu.menu).find('li').show();
contextMenu.show(e);
});
if ($mt.children().length) {
$mt.off('click dblclick preview');
$mt.on('preview', onPreview($mt));
if ($mt.find('img').length) {
$mt.on('click dblclick', function () {
$mt.trigger('preview');
});
}
return;
}
MediaTag(el);
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
@ -437,10 +678,10 @@ define([
observer.disconnect();
}
});
$mt.off('dblclick preview');
$mt.off('click dblclick preview');
$mt.on('preview', onPreview($mt));
if ($mt.find('img').length) {
$mt.on('dblclick', function () {
$mt.on('click dblclick', function () {
$mt.trigger('preview');
});
}
@ -461,8 +702,10 @@ define([
if (target) { target.scrollIntoView(); }
});
// loop over mermaid elements in the rendered content
$content.find('pre.mermaid').each(function (index, el) {
// loop over plugin elements in the rendered content
$content.find('pre[data-plugin]').each(function (index, el) {
var plugin = plugins[el.getAttribute('data-plugin')];
if (!plugin) { return; }
var $el = $(el);
$el.off('contextmenu').on('contextmenu', function (e) {
e.preventDefault();
@ -470,28 +713,23 @@ define([
$(contextMenu.menu).find('li:not(.cp-svg)').hide();
contextMenu.show(e);
});
$el.off('dblclick preview');
$el.off('dblclick click preview');
$el.on('preview', onPreview($el));
$el.on('dblclick', function () {
$el.on('dblclick click', function () {
$el.trigger('preview');
});
// since you've simply drawn the content that was supplied via markdown
// you can assume that the index of your rendered charts matches that
// of those in the markdown source.
var src = mermaid_source[index];
el.setAttribute('mermaid-source', src);
var cached = mermaid_cache[src];
var src = plugin.source[index];
el.setAttribute(plugin.attr, src);
var cached = getAvailableCachedElement($content, plugin.cache, src);
// check if you had cached a pre-rendered instance of the supplied source
if (typeof(cached) !== 'object') {
try {
Mermaid.init(undefined, $el);
// clickable elements in mermaid don't work well with our sandboxing setup
// the function below strips clickable elements but still leaves behind some artifacts
// tippy tooltips might still be useful, so they're not removed. It would be
// preferable to just support links, but this covers up a rough edge in the meantime
removeMermaidClickables($el);
plugin.render($el);
} catch (e) { console.error(e); }
return;
}
@ -506,6 +744,8 @@ define([
el.setAttribute('data-processed', true);
});
}
clearUnusedCacheEntries($content, plugins);
// recover the previous scroll position to avoid jank
$parent.scrollTop(scrollTop);
};

File diff suppressed because it is too large Load Diff

View File

@ -110,7 +110,8 @@ define([
}));
}).nThen(function (waitFor) {
var curve = $el.attr('data-curve');
var friend = curve === user.curvePublic ? user : friends[curve];
if (curve === user.curvePublic) { return; }
var friend = friends[curve];
if (!friend) { return; }
common.mailbox.sendTo("RM_OWNER", {
channel: channel,
@ -205,10 +206,14 @@ define([
var $sel = $div.find('.cp-usergrid-user.cp-selected');
var sel = $sel.toArray();
if (!sel.length) { return; }
var addMe = false;
var toAdd = sel.map(function (el) {
var curve = $(el).attr('data-curve');
// If the pad is woned by a team, we can transfer ownership to ourselves
if (curve === user.curvePublic && teamOwner) { return priv.edPublic; }
// If the pad is owned by a team, we can transfer ownership to ourselves
if (curve === user.curvePublic && teamOwner) {
addMe = true;
return;
}
var friend = friends[curve];
if (!friend) { return; }
return friend.edPublic;
@ -282,10 +287,31 @@ define([
}
}));
}
}).nThen(function (waitFor) {
// Offer ownership to a friend
if (addMe) {
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
command: 'ADD_OWNERS',
value: [priv.edPublic],
teamId: teamOwner
}, waitFor(function (err, res) {
err = err || (res && res.error);
if (err) {
waitFor.abort();
redrawAll(true);
var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden
: Messages.error;
return void UI.warn(text);
}
}));
}
}).nThen(function (waitFor) {
sel.forEach(function (el) {
var curve = $(el).attr('data-curve');
var friend = curve === user.curvePublic ? user : friends[curve];
if (curve === user.curvePublic) { return; }
var friend = friends[curve];
if (!friend) { return; }
common.mailbox.sendTo("ADD_OWNER", {
channel: channel,
@ -346,8 +372,7 @@ define([
var parsed = Hash.parsePadUrl(data.href || data.roHref);
var owned = Modal.isOwned(Env, data);
var disabled = !owned || !parsed.hashData || parsed.hashData.type !== 'pad';
var allowDisabled = parsed.type === 'drive';
if (disabled || allowDisabled) { return void cb(); }
if (disabled) { return void cb(); }
opts = opts || {};
@ -780,7 +805,7 @@ define([
id: 'cp-app-prop-change-password',
style: 'flex: 1;'
});
var passwordOk = h('button', Messages.properties_changePasswordButton);
var passwordOk = h('button.btn', Messages.properties_changePasswordButton);
var changePass = h('span.cp-password-change-container', [
newPassword,
passwordOk
@ -849,7 +874,7 @@ define([
// Use hidden hash if needed (we're an owner of this pad so we know it is stored)
var useUnsafe = Util.find(priv, ['settings', 'security', 'unsafeLinks']);
var href = (priv.readOnly && data.roHref) ? data.roHref : data.href;
if (useUnsafe === false) {
if (useUnsafe !== true) {
var newParsed = Hash.parsePadUrl(href);
var newSecret = Hash.getSecrets(newParsed.type, newParsed.hash, newPass);
var newHash = Hash.getHiddenHashFromKeys(parsed.type, newSecret, {});
@ -872,6 +897,28 @@ define([
$d.append(changePass);
}
}
if (owned) {
var deleteOwned = h('button.btn.btn-danger-alt', [h('i.cptools.cptools-destroy'), Messages.fc_delete_owned]);
var spinner = UI.makeSpinner();
UI.confirmButton(deleteOwned, {
classes: 'btn-danger'
}, function () {
spinner.spin();
sframeChan.query('Q_DELETE_OWNED', {
teamId: typeof(owned) !== "boolean" ? owned : undefined,
channel: data.channel
}, function (err, obj) {
spinner.done();
UI.findCancelButton().click();
if (err || (obj && obj.error)) { UI.warn(Messages.error); }
});
});
$d.append(h('br'));
$d.append(h('div', [
deleteOwned,
spinner.spinner
]));
}
return $d;
};
var drawRight = function () {
@ -894,7 +941,7 @@ define([
if (parsed.hashData.type !== 'pad' || parsed.type === 'drive') { return h('div', content); }
// Request edit access
if (data.roHref && !data.href) {
if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref)) {
var requestButton = h('button.btn.btn-secondary.no-margin.cp-access-margin-right',
Messages.requestEdit_button);
var requestBlock = h('p', requestButton);

View File

@ -26,6 +26,17 @@ define([
// Cache of the avatars outer html (including <media-tag>)
var avatars = {};
MT.getMediaTag = function (common, data) {
var metadataMgr = common.getMetadataMgr();
var privateDat = metadataMgr.getPrivateData();
var origin = privateDat.fileHost || privateDat.origin;
var src = data.src = data.src.slice(0,1) === '/' ? origin + data.src : data.src;
return h('media-tag', {
src: src,
'data-crypto-key': 'cryptpad:'+data.key
});
};
MT.getCursorAvatar = function (cursor) {
var html = '<span class="cp-cursor-avatar">';
html += (cursor.avatar && avatars[cursor.avatar]) || '';
@ -245,19 +256,23 @@ define([
}
// Reset modal
$inner.find('media-tag, pre.mermaid').detach();
$inner.find('media-tag, pre[data-plugin]').detach();
$spinner.show();
// Check src and cryptkey
var cfg = tags[i];
var tag;
if (cfg.svg) {
$spinner.hide();
$inner.append(cfg.svg);
if (!cfg.render) {
$spinner.hide();
locked = false;
return;
}
setTimeout(cfg.render);
tag = cfg.svg;
} else {
var src = cfg.src;
var key = cfg.key;
if (cfg.href) {
@ -273,12 +288,19 @@ define([
$spinner.hide();
return void UI.log(Messages.error);
}
var tag = h('media-tag', {
tag = h('media-tag', {
src: src,
'data-crypto-key': key
});
$inner.append(tag);
setTimeout(function () {
MediaTag(tag).on('error', function () {
locked = false;
$spinner.hide();
UI.log(Messages.error);
});
});
}
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function() {
@ -291,11 +313,6 @@ define([
childList: true,
characterData: false
});
MediaTag(tag).on('error', function () {
locked = false;
$spinner.hide();
UI.log(Messages.error);
});
};
show(i);
@ -382,14 +399,16 @@ define([
e.stopPropagation();
m.hide();
var $mt = $menu.data('mediatag');
if ($(this).hasClass("cp-app-code-context-saveindrive")) {
var $this = $(this);
if ($this.hasClass("cp-app-code-context-saveindrive")) {
common.importMediaTag($mt);
}
else if ($(this).hasClass("cp-app-code-context-download")) {
var media = $mt[0]._mediaObject;
else if ($this.hasClass("cp-app-code-context-download")) {
var media = Util.find($mt, [0, '_mediaObject']);
if (!(media && media._blob)) { return void console.error($mt); }
window.saveAs(media._blob.content, media.name);
}
else if ($(this).hasClass("cp-app-code-context-open")) {
else if ($this.hasClass("cp-app-code-context-open")) {
$mt.trigger('preview');
}
});

View File

@ -42,6 +42,7 @@ define([
// 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;
} else {
waitFor.abort();
@ -59,7 +60,7 @@ define([
}), opts.href);
// If this is a file, don't try to look for metadata
if (opts.channel && opts.channel.length > 34) { return; }
if (opts.channel && opts.channel.length > 32) { return; }
if (opts.channel) { data.channel = opts.channel; }
Modal.loadMetadata(Env, data, waitFor);
}).nThen(function () {
@ -69,23 +70,7 @@ define([
Modal.isOwned = function (Env, data) {
var common = Env.common;
data = data || {};
var priv = common.getMetadataMgr().getPrivateData();
var edPublic = priv.edPublic;
var owned = false;
if (Array.isArray(data.owners) && data.owners.length) {
if (data.owners.indexOf(edPublic) !== -1) {
owned = true;
} else {
Object.keys(priv.teams || {}).some(function (id) {
var team = priv.teams[id] || {};
if (team.viewer) { return; }
if (data.owners.indexOf(team.edPublic) === -1) { return; }
owned = Number(id);
return true;
});
}
}
return owned;
return common.isOwned(data.owners);
};
var blocked = false;
@ -129,7 +114,10 @@ define([
tabs[i] = {
content: c && UI.dialog.customModal(node, {
buttons: obj.buttons || button,
onClose: function () { blocked = false; }
onClose: function () {
blocked = false;
if (typeof(opts.onClose) === "function") { opts.onClose(); }
}
}),
disabled: !c,
title: obj.title,

11
www/common/less.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -329,7 +329,7 @@ define([
var priv = metadataMgr.getPrivateData();
var closeTips = h('span.fa.fa-window-close.cp-app-contacts-tips-close');
var closeTips = h('span.fa.fa-times.cp-app-contacts-tips-close');
var tips;
if (isApp && Util.find(priv.settings, ['general', 'hidetips', 'chat']) !== true) {
tips = h('div.cp-app-contacts-tips', [
@ -831,7 +831,6 @@ define([
var md = common.getMetadataMgr().getMetadata();
var name = md.title || md.defaultTitle;
$userlist.find(dataQuery(padChat)).find('.cp-app-contacts-name').text(name);
$userlist.find(dataQuery(padChat)).attr('title', name);
$messages.find(dataQuery(padChat) + ' .cp-app-contacts-header .cp-app-contacts-name')
.text(name);

View File

@ -48,7 +48,6 @@ define(['json.sortify'], function (Sortify) {
//title: meta.doc.defaultTitle,
type: meta.doc.type,
users: {},
authors: {}
};
metadataLazyObj = JSON.parse(JSON.stringify(metadataObj));
}
@ -69,6 +68,10 @@ define(['json.sortify'], function (Sortify) {
}
metadataObj.users = mdo;
// Clean old data
delete metadataObj.authors;
delete metadataLazyObj.authors;
// Always update the userlist in the lazy object, otherwise it may be outdated
// and metadataMgr.updateMetadata() won't do anything, and so we won't push events
// to the userlist UI ==> phantom viewers
@ -96,27 +99,6 @@ define(['json.sortify'], function (Sortify) {
checkUpdate(lazy);
});
};
var addAuthor = function () {
if (!meta.user || !meta.user.netfluxId || !priv || !priv.edPublic) { return; }
var authors = metadataObj.authors || {};
var old = Sortify(authors);
if (!authors[priv.edPublic]) {
authors[priv.edPublic] = {
nId: [meta.user.netfluxId],
name: meta.user.name
};
} else {
authors[priv.edPublic].name = meta.user.name;
if (authors[priv.edPublic].nId.indexOf(meta.user.netfluxId) === -1) {
authors[priv.edPublic].nId.push(meta.user.netfluxId);
}
}
if (Sortify(authors) !== old) {
metadataObj.authors = authors;
metadataLazyObj.authors = JSON.parse(JSON.stringify(authors));
change();
}
};
var netfluxId;
var isReady = false;
@ -225,7 +207,6 @@ define(['json.sortify'], function (Sortify) {
if (isReady) { return void f(); }
readyHandlers.push(f);
},
addAuthor: addAuthor,
});
};
return Object.freeze({ create: create });

View File

@ -4,10 +4,13 @@ define([
'/common/common-hash.js',
'/common/common-util.js',
'/common/common-messaging.js',
'/common/cryptget.js',
'/common/outer/mailbox.js',
'/customize/messages.js',
'/common/common-realtime.js',
'/bower_components/nthen/index.js',
'/bower_components/chainpad-crypto/crypto.js',
], function (AppConfig, Feedback, Hash, Util, Messaging, Mailbox, nThen, Crypto) {
], function (AppConfig, Feedback, Hash, Util, Messaging, Crypt, Mailbox, Messages, Realtime, nThen, Crypto) {
// Start migration check
// Versions:
// 1: migrate pad attributes
@ -320,6 +323,172 @@ define([
Feedback.send('Migrate-9', true);
userObject.version = version = 9;
}
}).nThen(function (waitFor) {
// Migration 10: deprecate todo
var fixTodo = function () {
var h = store.proxy.todo;
if (!h) { return; }
var next = waitFor(function () {
Feedback.send('Migrate-10', true);
userObject.version = version = 10;
});
var old;
var opts = {
network: store.network,
initialState: '{}',
metadata: {
owners: store.proxy.edPublic ? [store.proxy.edPublic] : []
}
};
nThen(function (w) {
Crypt.get(h, w(function (err, val) {
if (err || !val) {
w.abort();
next();
return;
}
try {
old = JSON.parse(val);
} catch (e) {} // We will abort in the next step in case of error
}), opts);
}).nThen(function (w) {
if (!old || typeof(old) !== "object") {
w.abort();
next();
return;
}
var k = {
content: {
data: {
"1": {
id: "1",
color: 'color6',
item: [],
title: Messages.kanban_todo
},
"2": {
id: "2",
color: 'color3',
item: [],
title: Messages.kanban_working
},
"3": {
id: "3",
color: 'color5',
item: [],
title: Messages.kanban_done
},
},
items: {},
list: [1, 2, 3]
},
metadata: {
title: Messages.type.todo,
defaultTitle: Messages.type.todo,
type: "kanban"
}
};
var i = 4;
var items = false;
(old.order || []).forEach(function (key) {
var data = old.data[key];
if (!data || !data.task) { return; }
items = true;
var column = data.state ? '3' : '1';
k.content.data[column].item.push(i);
k.content.items[i] = {
id: i,
title: data.task
};
i++;
});
if (!items) {
w.abort();
next();
return;
}
var newH = Hash.createRandomHash('kanban');
var secret = Hash.getSecrets('kanban', newH);
var oldSecret = Hash.getSecrets('todo', h);
Crypt.put(newH, JSON.stringify(k), w(function (err) {
if (err) {
w.abort();
next();
return;
}
if (store.rpc) {
store.rpc.pin([secret.channel], function () {
// Try to pin and ignore errors...
// Todo won't be available anyway so keep your unpinned kanban
});
store.rpc.unpin([oldSecret.channel], function () {
// Try to unpin and ignore errors...
});
}
var href = Hash.hashToHref(newH, 'kanban');
store.manager.addPad(['root'], {
title: Messages.type.todo,
owners: opts.metadata.owners,
channel: secret.channel,
href: href,
roHref: Hash.hashToHref(Hash.getViewHashFromKeys(secret), 'kanban'),
atime: +new Date(),
ctime: +new Date()
}, w(function (e) {
if (e) { return void console.error(e); }
delete store.proxy.todo;
var myData = Messaging.createData(userObject);
var ctx = { store: store };
Mailbox.sendTo(ctx, 'MOVE_TODO', {
user: myData,
href: href,
}, {
channel: myData.notifications,
curvePublic: myData.curvePublic
}, function (obj) {
if (obj && obj.error) { return void console.error(obj); }
});
}));
}), opts);
}).nThen(function () {
next();
});
};
if (version < 10) {
fixTodo();
}
}).nThen(function (waitFor) {
if (version >= 11) { return; }
// Migration 11: alert users of safe links as the new default
var done = function () {
Feedback.send('Migrate-11', true);
userObject.version = version = 11;
};
/* userObject.settings.security.unsafeLinks
undefined => the user has never touched it
false => the user has explicitly enabled "safe links"
true => the user has explicitly disabled "safe links"
*/
var unsafeLinks = Util.find(userObject, [ 'settings', 'security', 'unsafeLinks' ]);
if (unsafeLinks !== undefined) { return void done(); }
var ctx = {
store: store,
};
var myData = Messaging.createData(userObject);
if (!myData.curvePublic) { return void done(); }
Mailbox.sendTo(ctx, 'SAFE_LINKS_DEFAULT', {
user: myData,
}, {
channel: myData.notifications,
curvePublic: myData.curvePublic
}, waitFor(function (obj) {
if (obj && obj.error) { return void console.error(obj); }
done();
}));
/*}).nThen(function (waitFor) {
// Test progress bar in the loading screen
var i = 0;
@ -331,7 +500,7 @@ define([
}, 500);
progress(0, 0);*/
}).nThen(function () {
setTimeout(cb);
Realtime.whenRealtimeSyncs(store.realtime, Util.mkAsync(Util.bake(cb)));
});
};
});

View File

@ -8,17 +8,17 @@ define([
'/common/common-constants.js',
'/customize/messages.js',
'/bower_components/nthen/index.js'
], function ($, h, Hash, UI, UIElements, Util, Constants, Messages, nThen) {
], function($, h, Hash, UI, UIElements, Util, Constants, Messages, nThen) {
var handlers = {};
var defaultDismiss = function (common, data) {
return function (e) {
var defaultDismiss = function(common, data) {
return function(e) {
if (e) {
e.preventDefault();
e.stopPropagation();
}
common.mailbox.dismiss(data, function (err) {
common.mailbox.dismiss(data, function(err) {
if (err) { return void console.error(err); }
});
};
@ -26,7 +26,7 @@ define([
// Friend request
handlers['FRIEND_REQUEST'] = function (common, data) {
handlers['FRIEND_REQUEST'] = function(common, data) {
var content = data.content;
var msg = content.msg;
var userData = msg.content.user || msg.content;
@ -34,7 +34,7 @@ define([
msg.content = { user: userData };
// Display the notification
content.getFormatText = function () {
content.getFormatText = function() {
return Messages._getKey('friendRequest_notification', [name]);
};
@ -43,14 +43,14 @@ define([
// if not archived, add handlers
if (!content.archived) {
content.handler = function () {
content.handler = function() {
UIElements.displayFriendRequestModal(common, data);
};
common.addFriendRequest(data);
}
};
handlers['FRIEND_REQUEST_ACCEPTED'] = function (common, data) {
handlers['FRIEND_REQUEST_ACCEPTED'] = function(common, data) {
var content = data.content;
var msg = content.msg;
var userData = typeof(msg.content.user) === "object" ? msg.content.user : {
@ -58,7 +58,7 @@ define([
curvePublic: msg.content.user
};
var name = Util.fixHTML(userData.displayName) || Messages.anonymous;
content.getFormatText = function () {
content.getFormatText = function() {
return Messages._getKey('friendRequest_accepted', [name]);
};
if (!content.archived) {
@ -66,7 +66,7 @@ define([
}
};
handlers['FRIEND_REQUEST_DECLINED'] = function (common, data) {
handlers['FRIEND_REQUEST_DECLINED'] = function(common, data) {
var content = data.content;
var msg = content.msg;
var userData = typeof(msg.content.user) === "object" ? msg.content.user : {
@ -74,7 +74,7 @@ define([
curvePublic: msg.content.user
};
var name = Util.fixHTML(userData.displayName) || Messages.anonymous;
content.getFormatText = function () {
content.getFormatText = function() {
return Messages._getKey('friendRequest_declined', [name]);
};
if (!content.archived) {
@ -83,30 +83,44 @@ define([
};
// Share pad
handlers['SHARE_PAD'] = function (common, data) {
handlers['SHARE_PAD'] = function(common, data) {
var content = data.content;
var msg = content.msg;
var type = Hash.parsePadUrl(msg.content.href).type;
var key = type === 'drive' ? 'notification_folderShared' :
(type === 'file' ? 'notification_fileShared' :
'notification_padShared');
var teamNotification = /^team-/.test(data.type) && Number(data.type.slice(5));
var teamName = '';
if (teamNotification) {
var privateData = common.getMetadataMgr().getPrivateData();
var teamsData = Util.tryParse(JSON.stringify(privateData.teams)) || {};
var team = teamsData[teamNotification];
if (!team || !team.name) { return; }
key += "Team";
teamName = Util.fixHTML(team.name);
}
var name = Util.fixHTML(msg.content.name) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
content.getFormatText = function () {
return Messages._getKey(key, [name, title]);
content.getFormatText = function() {
return Messages._getKey(key, [name, title, teamName]);
};
content.handler = function () {
var todo = function () {
content.handler = function() {
var todo = function() {
common.openURL(msg.content.href);
defaultDismiss(common, data)();
};
nThen(function (waitFor) {
nThen(function(waitFor) {
if (msg.content.isTemplate) {
common.sessionStorage.put(Constants.newPadPathKey, ['template'], waitFor());
}
if (teamNotification) {
common.sessionStorage.put(Constants.newPadTeamKey, teamNotification, waitFor());
}
common.sessionStorage.put('newPadPassword', msg.content.password || '', waitFor());
}).nThen(function () {
}).nThen(function() {
todo();
});
};
@ -116,12 +130,12 @@ define([
};
// New support message from the admins
handlers['SUPPORT_MESSAGE'] = function (common, data) {
handlers['SUPPORT_MESSAGE'] = function(common, data) {
var content = data.content;
content.getFormatText = function () {
content.getFormatText = function() {
return Messages.support_notification;
};
content.handler = function () {
content.handler = function() {
common.openURL('/support/');
defaultDismiss(common, data)();
};
@ -130,7 +144,7 @@ define([
}
};
handlers['REQUEST_PAD_ACCESS'] = function (common, data) {
handlers['REQUEST_PAD_ACCESS'] = function(common, data) {
var content = data.content;
var msg = content.msg;
@ -140,13 +154,13 @@ define([
// Display the notification
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
content.getFormatText = function () {
content.getFormatText = function() {
return Messages._getKey('requestEdit_request', [title, name]);
};
// if not archived, add handlers
if (!content.archived) {
content.handler = function () {
content.handler = function() {
var link = h('a', {
href: '#'
}, Messages.requestEdit_viewPad);
@ -160,12 +174,12 @@ define([
verified,
link
]);
$(link).click(function (e) {
$(link).click(function(e) {
e.preventDefault();
e.stopPropagation();
common.openURL(msg.content.href);
});
UI.confirm(div, function (yes) {
UI.confirm(div, function(yes) {
if (!yes) { return; }
common.getSframeChannel().event('EV_GIVE_ACCESS', {
channel: msg.content.channel,
@ -182,7 +196,7 @@ define([
}
};
handlers['GIVE_PAD_ACCESS'] = function (common, data) {
handlers['GIVE_PAD_ACCESS'] = function(common, data) {
var content = data.content;
var msg = content.msg;
@ -195,26 +209,26 @@ define([
var title = Util.fixHTML(msg.content.title);
// Display the notification
content.getFormatText = function () {
content.getFormatText = function() {
return Messages._getKey('requestEdit_accepted', [title, name]);
};
// if not archived, add handlers
content.handler = function () {
content.handler = function() {
common.openURL(msg.content.href);
defaultDismiss(common, data)();
};
};
handlers['ADD_OWNER'] = function (common, data) {
handlers['ADD_OWNER'] = function(common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
content.getFormatText = function () {
content.getFormatText = function() {
return Messages._getKey('owner_request', [name, title]);
};
@ -223,7 +237,7 @@ define([
// if not archived, add handlers
if (!content.archived) {
content.handler = function () {
content.handler = function() {
if (msg.content.teamChannel) {
return void UIElements.displayAddTeamOwnerModal(common, data);
}
@ -232,7 +246,7 @@ define([
}
};
handlers['ADD_OWNER_ANSWER'] = function (common, data) {
handlers['ADD_OWNER_ANSWER'] = function(common, data) {
var content = data.content;
var msg = content.msg;
@ -240,7 +254,7 @@ define([
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
var key = 'owner_request_' + (msg.content.answer ? 'accepted' : 'declined');
content.getFormatText = function () {
content.getFormatText = function() {
return Messages._getKey(key, [name, title]);
};
if (!content.archived) {
@ -248,7 +262,7 @@ define([
}
};
handlers['RM_OWNER'] = function (common, data) {
handlers['RM_OWNER'] = function(common, data) {
var content = data.content;
var msg = content.msg;
@ -256,7 +270,7 @@ define([
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
var key = 'owner_removed' + (msg.content.pending ? 'Pending' : '');
content.getFormatText = function () {
content.getFormatText = function() {
return Messages._getKey(key, [name, title]);
};
if (!content.archived) {
@ -264,32 +278,32 @@ define([
}
};
handlers['INVITE_TO_TEAM'] = function (common, data) {
handlers['INVITE_TO_TEAM'] = function(common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var teamName = Util.fixHTML(Util.find(msg, ['content', 'team', 'metadata', 'name']) || '');
content.getFormatText = function () {
var teamName = Util.fixHTML(Util.find(msg, ['content', 'team', 'metadata', 'name'])  || '');
content.getFormatText = function() {
var text = Messages._getKey('team_invitedToTeam', [name, teamName]);
return text;
};
if (!content.archived) {
content.handler = function () {
content.handler = function() {
UIElements.displayInviteTeamModal(common, data);
};
}
};
handlers['KICKED_FROM_TEAM'] = function (common, data) {
handlers['KICKED_FROM_TEAM'] = function(common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var teamName = Util.fixHTML(Util.find(msg, ['content', 'teamName']) || '');
content.getFormatText = function () {
var teamName = Util.fixHTML(Util.find(msg, ['content', 'teamName'])  || '');
content.getFormatText = function() {
var text = Messages._getKey('team_kickedFromTeam', [name, teamName]);
return text;
};
@ -298,16 +312,16 @@ define([
}
};
handlers['INVITE_TO_TEAM_ANSWER'] = function (common, data) {
handlers['INVITE_TO_TEAM_ANSWER'] = function(common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var teamName = Util.fixHTML(Util.find(msg, ['content', 'team', 'metadata', 'name']) || '') ||
var teamName = Util.fixHTML(Util.find(msg, ['content', 'team', 'metadata', 'name'])  || '') ||
Util.fixHTML(Util.find(msg, ['content', 'teamName']));
var key = 'team_' + (msg.content.answer ? 'accept' : 'decline') + 'Invitation';
content.getFormatText = function () {
content.getFormatText = function() {
return Messages._getKey(key, [name, teamName]);
};
if (!content.archived) {
@ -315,11 +329,96 @@ define([
}
};
handlers['COMMENT_REPLY'] = function(common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
//var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var comment = Util.fixHTML(msg.content.comment).slice(0, 20).trim();
if (msg.content.comment.length > 20) {
comment += '...';
}
var title = Util.fixHTML(msg.content.title || Messages.unknownPad);
var href = msg.content.href;
content.getFormatText = function() {
return Messages._getKey('comments_notification', [comment, title]);
};
if (href) {
content.handler = function() {
common.openURL(href);
defaultDismiss(common, data)();
};
}
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
handlers['MENTION'] = function(common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title || Messages.unknownPad);
var href = msg.content.href;
content.getFormatText = function() {
return Messages._getKey('mentions_notification', [name, title]);
};
if (href) {
content.handler = function() {
common.openURL(href);
defaultDismiss(common, data)();
};
}
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
handlers['MOVE_TODO'] = function(common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
var title = Util.fixHTML(Messages.type.todo);
var href = msg.content.href;
content.getFormatText = function() {
return Messages._getKey('todo_move', [title]);
};
if (href) {
content.handler = function() {
common.openURL(href);
defaultDismiss(common, data)();
};
}
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
handlers['SAFE_LINKS_DEFAULT'] = function (common, data) {
var content = data.content;
content.getFormatText = function () {
return Messages.settings_safeLinkDefault;
};
content.handler = function () {
common.openURL('/settings/#security');
};
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
// NOTE: don't forget to fixHTML everything returned by "getFormatText"
return {
add: function (common, data) {
add: function(common, data) {
var type = data.content.msg.type;
if (handlers[type]) {
@ -332,7 +431,7 @@ define([
data.content.isDismissible = typeof data.content.dismissHandler === "function";
}
},
remove: function (common, data) {
remove: function(common, data) {
common.removeFriendRequest(data.hash);
},
allowed: Object.keys(handlers)

47
www/common/onlyoffice/AllFonts.js Normal file → Executable file

File diff suppressed because one or more lines are too long

View File

@ -63,6 +63,9 @@ body.cp-app-sheet, body.cp-app-oodoc, body.cp-app-ooslide {
display: flex;
flex-flow: column;
position: relative;
iframe {
flex: 1;
}
}
#cp-app-oo-offline {
position: absolute;

BIN
www/common/onlyoffice/fonts_thumbnail.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

BIN
www/common/onlyoffice/fonts_thumbnail@2x.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

@ -1,6 +1,6 @@
define([
'jquery',
'/common/toolbar3.js',
'/common/toolbar.js',
'json.sortify',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
@ -52,10 +52,11 @@ define([
$: $
};
var CHECKPOINT_INTERVAL = 50;
var CHECKPOINT_INTERVAL = 100;
var DISPLAY_RESTORE_BUTTON = false;
var NEW_VERSION = 2;
var NEW_VERSION = 3;
var PENDING_TIMEOUT = 30000;
var READONLY_REFRESH_TO = 15000;
var debug = function (x) {
if (!window.CP_DEV_MODE) { return; }
@ -76,6 +77,7 @@ define([
var privateData = metadataMgr.getPrivateData();
var readOnly = false;
var offline = false;
var ooLoaded = false;
var pendingChanges = {};
var config = {};
var content = {
@ -95,6 +97,8 @@ define([
// This structure is used for caching media data and blob urls for each media cryptpad url
var mediasData = {};
var startOO = function () {};
var getMediasSources = APP.getMediasSources = function() {
content.mediasSources = content.mediasSources || {};
return content.mediasSources;
@ -108,16 +112,17 @@ define([
return window.frames[0].editor || window.frames[0].editorCell;
};
var setEditable = function (state) {
var setEditable = function (state, force) {
$('#cp-app-oo-editor').find('#cp-app-oo-offline').remove();
/*
try {
getEditor().asc_setViewMode(!state);
//window.frames[0].editor.setViewModeDisconnect(true);
} catch (e) {}
if (!state && !readOnly) {
*/
if (!state && (!readOnly || force)) {
$('#cp-app-oo-editor').append(h('div#cp-app-oo-offline'));
}
debug(state);
};
var deleteOffline = function () {
@ -132,6 +137,12 @@ define([
APP.onLocal();
};
var isRegisteredUserOnline = function () {
var users = metadataMgr.getMetadata().users || {};
return Object.keys(users).some(function (id) {
return users[id] && users[id].curvePublic;
});
};
var isUserOnline = function (ooid) {
// Remove ids for users that have left the channel
deleteOffline();
@ -213,10 +224,17 @@ define([
var now = function () { return +new Date(); };
var getLastCp = function (old) {
var sortCpIndex = function (hashes) {
return Object.keys(hashes).map(Number).sort(function (a, b) {
return a-b;
});
};
var getLastCp = function (old, i) {
var hashes = old ? oldHashes : content.hashes;
if (!hashes || !Object.keys(hashes).length) { return {}; }
var lastIndex = Math.max.apply(null, Object.keys(hashes).map(Number));
i = i || 0;
var idx = sortCpIndex(hashes);
var lastIndex = idx[idx.length - 1 - i];
var last = JSON.parse(JSON.stringify(hashes[lastIndex]));
return last;
};
@ -254,11 +272,27 @@ define([
}
};
/*
var checkDrawings = function () {
var editor = getEditor();
if (!editor || !editor.GetSheets) { return false; }
var s = editor.GetSheets();
return s.some(function (obj) {
return obj.worksheet.Drawings.length;
});
};
*/
// DEPRECATED from version 3
// Loading a checkpoint reorder the sheet starting from ID "5".
// We have to reorder it manually when a checkpoint is created
// so that the messages we send to the realtime channel are
// loadable by users joining after the checkpoint
var fixSheets = function () {
// Starting from version 3, we don't need to fix the sheet IDs anymore
// because we reload onlyoffice whenever we receive a checkpoint
if (!APP.migrate || (content && content.version > 2)) { return; }
try {
var editor = getEditor();
// if we are not in the sheet app
@ -283,13 +317,21 @@ define([
console.error(err);
return void UI.alert(Messages.oo_saveError);
}
var i = Math.floor(ev.index / CHECKPOINT_INTERVAL);
// Get the last cp idx
var all = sortCpIndex(content.hashes || {});
var current = all[all.length - 1] || 0;
// Get the expected cp idx
var _i = Math.floor(ev.index / CHECKPOINT_INTERVAL);
// Take the max of both
var i = Math.max(_i, current);
content.hashes[i] = {
file: data.url,
hash: ev.hash,
index: ev.index
};
oldHashes = JSON.parse(JSON.stringify(content.hashes));
content.locks = {};
content.ids = {};
// If this is a migration, set the new version
if (APP.migrate) {
delete content.migration;
@ -336,6 +378,35 @@ define([
};
APP.FM = common.createFileManager(fmConfig);
// Add a lock
var isLockedModal = {
content: UI.dialog.customModal(h('div.cp-oo-x2tXls', [
h('span.fa.fa-spin.fa-spinner'),
h('span', Messages.oo_isLocked)
]))
};
var resetData = function (blob, type) {
// If a read-only refresh popup was planned, abort it
delete APP.refreshPopup;
clearTimeout(APP.refreshRoTo);
if (!isLockedModal.modal) {
isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
}
myUniqueOOId = undefined;
setMyId();
APP.docEditor.destroyEditor(); // Kill the old editor
$('iframe[name="frameEditor"]').after(h('div#cp-app-oo-placeholder')).remove();
ooLoaded = false;
oldLocks = {};
Object.keys(pendingChanges).forEach(function (key) {
clearTimeout(pendingChanges[key]);
delete pendingChanges[key];
});
startOO(blob, type, true);
};
var saveToServer = function () {
var text = getContent();
var blob = new Blob([text], {type: 'plain/text'});
@ -346,18 +417,52 @@ define([
index: ooChannel.cpIndex
};
fixSheets();
ooChannel.ready = false;
ooChannel.queue = [];
data.callback = function () {
resetData(blob, file);
};
APP.FM.handleFile(blob, data);
};
var noLogin = false;
var makeCheckpoint = function (force) {
if (!common.isLoggedIn()) { return; }
var locked = content.saveLock;
var lastCp = getLastCp();
var needCp = force || ooChannel.cpIndex % CHECKPOINT_INTERVAL === 0 ||
(ooChannel.cpIndex - lastCp.index) > CHECKPOINT_INTERVAL;
(ooChannel.cpIndex - (lastCp.index || 0)) > CHECKPOINT_INTERVAL;
if (!needCp) { return; }
if (!locked || !isUserOnline(locked) || force) {
if (!common.isLoggedIn() && !isRegisteredUserOnline() && !noLogin) {
var login = h('button.cp-corner-primary', Messages.login_login);
var register = h('button.cp-corner-primary', Messages.login_register);
var cancel = h('button.cp-corner-cancel', Messages.cancel);
var actions = h('div', [cancel, register, login]);
var modal = UI.cornerPopup(Messages.oo_login, actions, '', {alt: true});
$(register).click(function () {
common.setLoginRedirect(function () {
common.gotoURL('/register/');
});
modal.delete();
});
$(login).click(function () {
common.setLoginRedirect(function () {
common.gotoURL('/login/');
});
modal.delete();
});
$(cancel).click(function () {
modal.delete();
noLogin = true;
});
return;
}
if (!common.isLoggedIn()) { return; }
content.saveLock = myOOId;
APP.onLocal();
APP.realtime.onSettle(function () {
@ -365,6 +470,20 @@ define([
});
}
};
var deleteLastCp = function () {
var hashes = content.hashes;
if (!hashes || !Object.keys(hashes).length) { return; }
var i = 0;
var idx = Object.keys(hashes).map(Number).sort(function (a, b) {
return a-b;
});
var lastIndex = idx[idx.length - 1 - i];
delete content.hashes[lastIndex];
APP.onLocal();
APP.realtime.onSettle(function () {
UI.log(Messages.saved);
});
};
var restoreLastCp = function () {
content.saveLock = myOOId;
APP.onLocal();
@ -395,6 +514,88 @@ define([
}, to);
};
var loadInitDocument = function (type, useNewDefault) {
var newText;
switch (type) {
case 'sheet' :
newText = EmptyCell(useNewDefault);
break;
case 'oodoc':
newText = EmptyDoc();
break;
case 'ooslide':
newText = EmptySlide();
break;
default:
newText = '';
}
return new Blob([newText], {type: 'text/plain'});
};
var loadLastDocument = function (lastCp, onCpError, cb) {
ooChannel.cpIndex = lastCp.index || 0;
ooChannel.lastHash = lastCp.hash;
var parsed = Hash.parsePadUrl(lastCp.file);
var secret = Hash.getSecrets('file', parsed.hash);
if (!secret || !secret.channel) { return; }
var hexFileName = secret.channel;
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (/^4/.test('' + this.status)) {
onCpError(this.status);
return void console.error('XHR error', this.status);
}
var arrayBuffer = xhr.response;
if (arrayBuffer) {
var u8 = new Uint8Array(arrayBuffer);
FileCrypto.decrypt(u8, key, function (err, decrypted) {
if (err) { return void console.error(err); }
var blob = new Blob([decrypted.content], {type: 'plain/text'});
if (cb) {
return cb(blob, getFileType());
}
startOO(blob, getFileType());
});
}
};
xhr.onerror = function (err) {
onCpError(err);
};
xhr.send(null);
};
var refreshReadOnly = function () {
var cancel = h('button.cp-corner-cancel', Messages.cancel);
var reload = h('button.cp-corner-primary', [
h('i.fa.fa-refresh'),
Messages.oo_refresh
]);
var actions = h('div', [cancel, reload]);
var m = UI.cornerPopup(Messages.oo_refreshText, actions, '');
$(reload).click(function () {
ooChannel.ready = false;
var lastCp = getLastCp();
loadLastDocument(lastCp, function () {
var file = getFileType();
var type = common.getMetadataMgr().getPrivateData().ooType;
var blob = loadInitDocument(type, true);
resetData(blob, file);
}, function (blob, file) {
resetData(blob, file);
});
delete APP.refreshPopup;
m.delete();
});
$(cancel).click(function () {
delete APP.refreshPopup;
m.delete();
});
};
var openRtChannel = function (cb) {
if (rtChannel.ready) { return void cb(); }
@ -412,12 +613,25 @@ define([
sframeChan.on('EV_OO_EVENT', function (obj) {
switch (obj.ev) {
case 'READY':
cb();
break;
case 'LEAVE':
removeClient(obj.data);
break;
case 'MESSAGE':
if (ooChannel.ready) {
// In read-only mode, push the message to the queue and prompt
// the user to refresh OO (without reloading the page)
if (readOnly) {
ooChannel.queue.push(obj.data);
if (APP.refreshPopup) { return; }
APP.refreshPopup = true;
// Don't "spam" the user instantly and no more than
// 1 popup every 15s
APP.refreshRoTo = setTimeout(refreshReadOnly, READONLY_REFRESH_TO);
return;
}
ooChannel.send(obj.data.msg);
ooChannel.lastHash = obj.data.hash;
ooChannel.cpIndex++;
@ -427,7 +641,6 @@ define([
break;
}
});
cb();
};
var getParticipants = function () {
@ -538,18 +751,26 @@ define([
};
var handleAuth = function (obj, send) {
// OO is ready
ooChannel.ready = true;
// Get the content pushed after the latest checkpoint
//setEditable(false);
var changes = [];
if (content.version > 2) {
ooChannel.queue.forEach(function (data) {
Array.prototype.push.apply(changes, data.msg.changes);
});
ooChannel.lastHash = getLastCp().hash;
ooChannel.ready = true;
ooChannel.cpIndex += ooChannel.queue.length;
var last = ooChannel.queue.pop();
if (last) { ooChannel.lastHash = last.hash; }
} else {
setEditable(false, true);
}
send({
type: "authChanges",
changes: changes
});
// Answer to the auth command
var p = getParticipants();
send({
@ -572,14 +793,6 @@ define([
type: "documentOpen",
data: {"type":"open","status":"ok","data":{"Editor.bin":obj.openCmd.url}}
});
// Update current index
var last = ooChannel.queue.pop();
if (last) { ooChannel.lastHash = last.hash; }
ooChannel.cpIndex += ooChannel.queue.length;
// Apply existing locks
deleteOfflineLocks();
APP.onLocal();
handleNewLocks(oldLocks, content.locks || {});
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
@ -598,13 +811,6 @@ define([
});
};
// Add a lock
var isLockedModal = {
content: UI.dialog.customModal(h('div.cp-oo-x2tXls', [
h('span.fa.fa-spin.fa-spinner'),
h('span', Messages.oo_isLocked)
]))
};
var handleLock = function (obj, send) {
if (content.saveLock) {
if (!isLockedModal.modal) {
@ -698,7 +904,12 @@ define([
return;
}
// XXX
// If save lock, tell onlyoffice that it can't save now...
// if (content && content.saveLock] {}
// Send the changes
content.locks = content.locks || {};
rtChannel.sendMsg({
type: "saveChanges",
changes: parseChanges(obj.changes),
@ -709,6 +920,12 @@ define([
if (err) {
return void console.error(err);
}
// XXX
// If save lock, it means the sheet was locked for a checkpoint before
// our message was received!
// Add our message to our own queue to load it after the checkpoint reload
if (pendingChanges[uid]) {
clearTimeout(pendingChanges[uid]);
delete pendingChanges[uid];
@ -811,18 +1028,25 @@ define([
index: -1
});
}
if (APP.onDocumentUnlock) {
APP.onDocumentUnlock();
APP.onDocumentUnlock = undefined;
}
break;
}
});
});
};
var ooLoaded = false;
var startOO = function (blob, file) {
if (APP.ooconfig) { return void console.error('already started'); }
startOO = function (blob, file, force) {
if (APP.ooconfig && !force) { return void console.error('already started'); }
var url = URL.createObjectURL(blob);
var lock = readOnly || APP.migrate;
// Starting from version 3, we can use the view mode again
// defined but never used
//var mode = (content && content.version > 2 && lock) ? "view" : "edit";
// Config
APP.ooconfig = {
"document": {
@ -847,7 +1071,7 @@ define([
"id": String(myOOId), //"c0c3bf82-20d7-4663-bf6d-7fa39c598b1d",
"firstname": metadataMgr.getUserData().name || Messages.anonymous,
},
"mode": lock ? "view" : "edit",
"mode": "edit",
"lang": (navigator.language || navigator.userLanguage || '').slice(0,2)
},
"events": {
@ -889,15 +1113,75 @@ define([
}
},
"onDocumentReady": function () {
if (APP.migrate && !readOnly) {
var onMigrateRdy = Util.mkEvent();
onMigrateRdy.reg(function () {
var div = h('div.cp-oo-x2tXls', [
h('span.fa.fa-spin.fa-spinner'),
h('span', Messages.oo_sheetMigration_loading)
h('span', Messages.oo_sheetMigration_loading) // XXX tell them that it will take ~ 1min)
]);
UI.openCustomModal(UI.dialog.customModal(div, {buttons: []}));
setTimeout(function () {
makeCheckpoint(true);
}, 1000);
});
// DEPRECATED: from version 3, the queue is sent again during init
if (APP.migrate && ((content.version || 1) <= 2)) {
// The doc is ready, fix the worksheets IDs and push the queue
fixSheets();
// Push changes since last cp
ooChannel.ready = true;
var changes = [];
var changesIndex;
ooChannel.queue.forEach(function (data) {
Array.prototype.push.apply(changes, data.msg.changes);
changesIndex = data.msg.changesIndex;
//ooChannel.send(data.msg);
});
ooChannel.cpIndex += ooChannel.queue.length;
var last = ooChannel.queue.pop();
if (last) { ooChannel.lastHash = last.hash; }
var onDocUnlock = function () {
// Migration required but read-only: continue...
if (readOnly) {
setEditable(true);
getEditor().setViewModeDisconnect();
} else {
// No changes after the cp: migrate now
onMigrateRdy.fire();
}
};
// Send the changes all at once
if (changes.length) {
setTimeout(function () {
ooChannel.send({
type: 'saveChanges',
changesIndex: changesIndex,
changes: changes,
locks: []
});
APP.onDocumentUnlock = onDocUnlock;
}, 5000);
return;
}
onDocUnlock();
return;
}
if (lock) {
getEditor().setViewModeDisconnect();
} else {
setEditable(true);
}
if (isLockedModal.modal && force) {
isLockedModal.modal.closeModal();
delete isLockedModal.modal;
$('#cp-app-oo-editor > iframe')[0].contentWindow.focus();
}
if (APP.migrate && !readOnly) {
onMigrateRdy.fire();
}
}
}
@ -907,8 +1191,19 @@ define([
if (ifr) { ifr.remove(); }
};
common.initFilePicker({
onSelect: function (data) {
APP.UploadImageFiles = function (files, type, id, jwt, cb) {
cb('NO');
};
APP.AddImage = function(cb1, cb2) {
APP.AddImageSuccessCallback = cb1;
APP.AddImageErrorCallback = cb2;
common.openFilePicker({
types: ['file'],
where: ['root'],
filter: {
fileType: ['image/']
}
}, function (data) {
if (data.type !== 'file') {
debug("Unexpected data type picked " + data.type);
return;
@ -923,25 +1218,13 @@ define([
APP.realtime.onSettle(function () {
APP.getImageURL(name, function(url) {
debug("CRYPTPAD success add " + name);
common.setPadAttribute('atime', +new Date(), null, data.href);
APP.AddImageSuccessCallback({
name: name,
url: url
});
});
});
}
});
APP.AddImage = function(cb1, cb2) {
APP.AddImageSuccessCallback = cb1;
APP.AddImageErrorCallback = cb2;
common.openFilePicker({
types: ['file'],
where: ['root'],
filter: {
fileType: ['image/']
}
});
};
@ -1091,12 +1374,12 @@ define([
var x2tSaveAndConvertData = function(data, filename, extension, finalFilename) {
// Perform the x2t conversion
require(['/common/onlyoffice/x2t/x2t.js'], function() {
require(['/common/onlyoffice/x2t/x2t.js'], function() { // FIXME why does this fail without an access-control-allow-origin header?
var x2t = window.Module;
x2t.run();
if (x2tInitialized) {
debug("x2t runtime already initialized");
x2tSaveAndConvertDataInternal(x2t, data, filename, extension, finalFilename);
return void x2tSaveAndConvertDataInternal(x2t, data, filename, extension, finalFilename);
}
x2t.onRuntimeInitialized = function() {
@ -1108,6 +1391,9 @@ define([
});
};
var supportsXLSX = function () {
return !(typeof(Atomics) === "undefined" || typeof (SharedArrayBuffer) === "undefined");
};
var exportXLSXFile = function() {
var text = getContent();
@ -1121,7 +1407,7 @@ define([
ext = ['.docx', /*'.odt',*/ '.bin'];
}
if (typeof(Atomics) === "undefined") {
if (!supportsXLSX()) {
ext = ['.bin'];
warning = '<div class="alert alert-info cp-alert-top">'+Messages.oo_exportChrome+'</div>';
}
@ -1168,6 +1454,7 @@ define([
}, {
typeInput: $select[0]
}, true);
$select.find('button').addClass('btn');
};
var x2tImportImagesInternal = function(x2t, images, i, callback) {
@ -1300,7 +1587,7 @@ define([
if (ext === "bin") {
return void importFile(content);
}
if (typeof(Atomics) === "undefined") {
if (!supportsXLSX()) {
return void UI.alert(Messages.oo_invalidFormat);
}
var div = h('div.cp-oo-x2tXls', [
@ -1331,43 +1618,20 @@ define([
}, 100);
};
var loadLastDocument = function () {
var lastCp = getLastCp();
if (!lastCp) { return; }
ooChannel.cpIndex = lastCp.index || 0;
var parsed = Hash.parsePadUrl(lastCp.file);
var secret = Hash.getSecrets('file', parsed.hash);
if (!secret || !secret.channel) { return; }
var hexFileName = secret.channel;
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (/^4/.test('' + this.status)) {
return void console.error('XHR error', this.status);
}
var arrayBuffer = xhr.response;
if (arrayBuffer) {
var u8 = new Uint8Array(arrayBuffer);
FileCrypto.decrypt(u8, key, function (err, decrypted) {
if (err) { return void console.error(err); }
var blob = new Blob([decrypted.content], {type: 'plain/text'});
startOO(blob, getFileType());
});
}
};
xhr.send(null);
};
var loadDocument = function (noCp, useNewDefault) {
var loadDocument = function (noCp, useNewDefault, i) {
if (ooLoaded) { return; }
var type = common.getMetadataMgr().getPrivateData().ooType;
var file = getFileType();
if (!noCp) {
var lastCp = getLastCp(false, i);
// If the last checkpoint is empty, load the "initial" doc instead
if (!lastCp || !lastCp.file) { return void loadDocument(true, useNewDefault); }
// Load latest checkpoint
return void loadLastDocument();
return void loadLastDocument(lastCp, function () {
// Checkpoint error: load the previous one
i = i || 0;
loadDocument(noCp, useNewDefault, ++i);
});
}
var newText;
switch (type) {
@ -1383,7 +1647,7 @@ define([
default:
newText = '';
}
var blob = new Blob([newText], {type: 'text/plain'});
var blob = loadInitDocument(type, useNewDefault);
startOO(blob, file);
};
@ -1444,23 +1708,14 @@ define([
};
config.onInit = function (info) {
readOnly = metadataMgr.getPrivateData().readOnly;
var privateData = metadataMgr.getPrivateData();
readOnly = privateData.readOnly;
Title = common.createTitle({});
var configTb = {
displayed: [
'chat',
'userlist',
'title',
'useradmin',
'spinner',
'newpad',
'share',
'limit',
'unpinnedWarning',
'notifications'
],
displayed: ['pad'],
title: Title.getTitleConfig(),
metadataMgr: metadataMgr,
readOnly: readOnly,
@ -1472,15 +1727,21 @@ define([
toolbar = APP.toolbar = Toolbar.create(configTb);
Title.setToolbar(toolbar);
var $rightside = toolbar.$rightside;
if (window.CP_DEV_MODE) {
var $save = common.createButton('save', true, {}, function () {
makeCheckpoint(true);
});
$save.appendTo($rightside);
$save.appendTo(toolbar.$bottomM);
}
if (window.CP_DEV_MODE || DISPLAY_RESTORE_BUTTON) {
common.createButton('', true, {
name: 'delete',
icon: 'fa-trash',
hiddenReadOnly: true
}).click(function () {
if (initializing) { return void console.error('initializing'); }
deleteLastCp();
}).attr('title', 'Delete last checkpoint').appendTo(toolbar.$bottomM);
common.createButton('', true, {
name: 'restore',
icon: 'fa-history',
@ -1488,47 +1749,48 @@ define([
}).click(function () {
if (initializing) { return void console.error('initializing'); }
restoreLastCp();
}).attr('title', 'Restore last checkpoint').appendTo($rightside);
}).attr('title', 'Restore last checkpoint').appendTo(toolbar.$bottomM);
}
var $exportXLSX = common.createButton('export', true, {}, exportXLSXFile);
$exportXLSX.appendTo($rightside);
$exportXLSX.appendTo(toolbar.$drawer);
var type = common.getMetadataMgr().getPrivateData().ooType;
var type = privateData.ooType;
var accept = [".bin", ".ods", ".xlsx"];
if (type === "ooslide") {
accept = ['.bin', '.odp', '.pptx'];
} else if (type === "oodoc") {
accept = ['.bin', '.odt', '.docx'];
}
if (typeof(Atomics) === "undefined") {
if (!supportsXLSX()) {
accept = ['.bin'];
}
if (common.isLoggedIn()) {
window.CryptPad_deleteLastCp = deleteLastCp;
var $importXLSX = common.createButton('import', true, {
accept: accept,
binary : ["ods", "xlsx", "odt", "docx", "odp", "pptx"]
}, importXLSXFile);
$importXLSX.appendTo($rightside);
if (common.isLoggedIn()) {
common.createButton('hashtag', true).appendTo($rightside);
$importXLSX.appendTo(toolbar.$drawer);
common.createButton('hashtag', true).appendTo(toolbar.$drawer);
}
var $forget = common.createButton('forget', true, {}, function (err) {
if (err) { return; }
setEditable(false);
});
$rightside.append($forget);
toolbar.$drawer.append($forget);
if (!privateData.isEmbed) {
var helpMenu = APP.helpMenu = common.createHelpMenu(['beta', 'oo']);
$('#cp-app-oo-editor').prepend(common.getBurnAfterReadingWarning());
$('#cp-app-oo-editor').prepend(helpMenu.menu);
toolbar.$drawer.append(helpMenu.button);
}
var $properties = common.createButton('properties', true);
toolbar.$drawer.append($properties);
var $access = common.createButton('access', true);
toolbar.$drawer.append($access);
};
config.onReady = function (info) {
@ -1566,6 +1828,7 @@ define([
}
var version = 'v2a/';
var msg;
// Old version detected: use the old OO and start the migration if we can
if (privateData.ooForceVersion) {
if (privateData.ooForceVersion === "1") {
@ -1574,12 +1837,23 @@ define([
} else if (content && (!content.version || content.version === 1)) {
version = 'v1/';
APP.migrate = true;
// Registedred users can start the migration
if (common.isLoggedIn()) {
// Registedred ~~users~~ editors can start the migration
if (common.isLoggedIn() && !readOnly) {
content.migration = true;
APP.onLocal();
} else {
var msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor);
msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor); // XXX update: "anonymous users or viewers"
$(APP.helpMenu.menu).after(msg);
readOnly = true;
}
} else if (content && content.version === 2) {
APP.migrate = true;
// Registedred ~~users~~ editors can start the migration
if (common.isLoggedIn() && !readOnly) {
content.migration = true;
APP.onLocal();
} else {
msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor);
$(APP.helpMenu.menu).after(msg);
readOnly = true;
}
@ -1625,6 +1899,17 @@ define([
var reloadPopup = false;
var checkNewCheckpoint = function () {
var lastCp = getLastCp();
loadLastDocument(lastCp, function (err) {
console.error(err);
// On error, do nothing
// XXX lock the document or ask for a page reload?
}, function (blob, type) {
resetData(blob, type);
});
};
config.onRemote = function () {
if (initializing) { return; }
var userDoc = APP.realtime.getUserDoc();
@ -1650,10 +1935,15 @@ define([
var latest = getLastCp(true);
var newLatest = getLastCp();
if (newLatest.index > latest.index) {
ooChannel.queue = [];
ooChannel.ready = false;
// New checkpoint
sframeChan.query('Q_OO_SAVE', {
hash: newLatest.hash,
url: newLatest.file
}, function () { });
}, function () {
checkNewCheckpoint();
});
}
oldHashes = JSON.parse(JSON.stringify(content.hashes));
}

Some files were not shown because too many files have changed in this diff Show More