diff --git a/CHANGELOG.md b/CHANGELOG.md index d33d0e25c..55d0e2534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,62 @@ +# 4.8.0 + +## Goals + +This release cycle we decided to give people a chance to try our forms app and provide feedback before we begin developing its second round of major features and improvements. In the meantime we planned to work mostly on the activities of our [NGI DAPSI](https://dapsi.ngi.eu/) project which concerns client-side file format conversions. Otherwise, we dedicated some of our independently funded time towards some internal code review and security best-practices as a follow-up to the recent quick-scan performed by [Radically Open Security](https://radicallyopensecurity.com/) that was funded by [NLnet](https://nlnet.nl) as a part of our now-closing _CryptPad for Communities_ project. + +## Update notes + +We are still accepting feedback concerning our Form application via [a form hosted on CryptPad.fr](https://cryptpad.fr/form/#/2/form/view/gYs4QS7DetInCXy0z2CQoUW6CwN6kaR2utGsftDzp58/). We will accept feedback here until July 12th, 2021, so if you'd like your opinions to be represented in the app's second round of development act quickly! + +Following our last release we sent out an email to the admins of each outdated instance that had included their addresses in the server's daily telemetry. This appears to have been successful, as more than half of the 700+ instances that provide this telemetry are now running **4.7.0**. Previously, only 15% of instances were running the latest version. It's worth noting that of those admins that are hosting the latest version, less than 10% have opted into future emails warning them of security issues. In case you missed it, this can be done on the admin panel's _Network_ tab. Unlike most companies, we consider excess data collection a liability rather than an asset. As such, adminstrator emails are no longer included in server telemetry unless the admin has consented to be contacted. + +The same HTTP request that communicates server telemetry will soon begin responding with the URL of our latest release notes if it is detected that the remote instance is running an older version. The admin panel's _Network_ tab for instances running 4.7.0 or later will begin prompting admins to view the release notes and update once 4.8.0 is available. + +The Network tab now includes a multiple choice form as well. If you have not disabled your instance's telemetry you can use this field to answer _why you run your instance_ (for a business, an academic institution, personal use, etc.). We intend to use this data to inform our development roadmap, though as always, the fastest way to get us to prioritize your needs is to contact us for a support contract (sales@cryptpad.fr). + +Server telemetry will also include an `installMethod` property. By default this is `"unspecified"`, but we are planning to work with packagers of alternate install methods to modify this property in their installation scripts. This will help us assess what proportion of instances are installed via the steps included in our installation guide vs other methods such as the various docker images. We hope that it will also allow us to determine the source of some common misconfigurations so we can propose some improvements to the root cause. + +Getting off the topic of telemetry: two types of data that were previously deleted outright (pin logs and login blocks) are now archived when the client sends a _remove_ command. This provides for the ability to restore old user credentials in cases where users claim that their new credentials do not work following a password change. Some discretion is required in such cases as a user might have intentionally invalidated their old credentials due to shoulder-surfing or the breach of another service's database where they'd reused credentials. Neither of these types of data are currently included in the scripts which evict old data as they are not likely to consume a significant amount of storage space. In any case, CryptPad's data is stored on the filesystem, so it's always possible to remove outdated files by removing them from `cryptpad/data/archive/*` or whatever path you've configured for your archives. + +This release introduces some minor changes to the provided NGINX configuration file to enable support for WebAssembly where it is required for client-side file format conversions. We've added some new tests on the /checkup/ page that determine whether these changes have been applied. This page can be found via a button on the admin panel. + +To update from 4.7.0 to 4.8.0: + +1. Apply the documented NGINX configuration +2. Stop your server +3. Get the latest code with git +4. Install the latest dependencies with `bower update` and `npm i` +5. Restart your server +6. Confirm that your instance is passing all the tests included on the `/checkup/` page + +## Features + +* Those who prefer using tools localized in Japanese can thank [@Suguru](https://mstdn.progressiv.dev/@suguru) for completing the Japanese translation of the platform's text! CryptPad is a fairly big platform with a lot of text to translate, so we really appreciate how much effort went into this. + * While we're on the topic, CryptPad's _Deutsch_ translation is kept up to date largely by a single member of the German Pirate Party (Piratenpartei Deutschland). This is a huge job and we appreciate your work too! + * Anyone else who wishes to give back to the project by doing the same can contribute translations on an ongoing basis through [our Weblate instance](https://weblate.cryptpad.fr/projects/cryptpad/app/). +* We've implemented a new app for file format conversions as a part of our _INTEROFFICE_ project. At this point this page is largely a test-case for the conversion engine that we hope to integrate more tightly into the rest of the platform. It allows users to load a variety of file formats into their browser and convert to any other format that has a defined conversion process from the original format. What's special about this is that files are converted entirely in your browser, unlike other platforms which do so in the cloud and expose their contents in the process. Currently we support conversion between the following formats in every browser that supports modern web standards (ie. not safari): + * XLSX and ODS + * DOCX and ODT and TXT + * PPTX and ODP +* In addition to the /convert/ page which supports office file formats, we also put some time into improving interoperability for our existing apps. We're introducing the ability to export rich text documents as Markdown (via the [turndown](https://github.com/mixmark-io/turndown) library), to import trello's JSON format into our Kanban app (with some loss of attributes because we don't support all the same features), and to export form summaries as CSV files. +* We've added another extension to our customized markdown renderer which replaces markdown images with a warning that CryptPad blocks remote content to prevent malicious users from tracking visitors to certain pages. Such images should already be blocked by our strict use of Content-Security-Policy headers, but this will provide a better indication why images are failing to load on isnstances that are correctly configured and a modest improvement to users' privacy on instances that aren't. +* Up until now it was possible to include style tags in markdown documents, which some of our more advanced users used in order to customize the appearance of their rendered documents. Unfortunately, these styles were not applied strictly to the markdown preview window, but to the page as a whole, making it possible to break the platform's interface (for that pad) through the use of overly broad and powerful style rules. As of this release style tags are now treated as special elements, such that their contents are compiled as [LESS](https://lesscss.org/) within a scope that is only applied to the preview pane. This was intended as a bug fix, but it's included here as a _feature_ because advanced users might see it as such and use it to do neat things. We have no funding for further work in this direction, however, and presently have no intent of providing documentation about this behaviour. +* The checkup page uses some slightly nicer methods of displaying values returned by tests when the expected value of `true` is not returned. Some tests have been revised to return the problematic value instead of `false` when the test fails, since there were some cases where it was not clear why the test was failing, such as when a header was present but duplicated. +* We've made some server requests related to _pinning files_ moderately faster by skipping an expensive calculation and omitting the value it returned. This value was meant to be used as a checksum to ensure that all of a user's documents were included in the list which should be associated with their account, however, clients used a separate command to fetch this checksum. The value provided in response to the other commands was never used by the client. +* We've implemented a system on the client for defining default templates for particular types of documents across an entire instance in addition to the use of documents in the _templates_ section of the users drive (or that of their teams). This is intended more as a generic system for us to reuse throughout the platform's source than an API for instance admins to use. If there is sufficient interest (and funding) from other admins we'll implement this as an instance configuration point. We now provide a _poll_ template to replicate the features of our old poll app which has been deprecated in favour of forms. +* We've included some more non-sensitive information about users' teams to the debugging data to which is automatically submitted along with support tickets, such as the id of the team's drive, roster, and how large the drive's contents are. +* The _Log out everywhere_ option that is displayed in the user admin menu in the top-right corner of the page for logged-in users now displays a confirmation before terminating all remote sessions. + +## Bug fixes + +* It was brought to our attention that the registration page was not trimming leading and trailing whitespace from usernames as intended. We've updated the page to do so, however, accounts created with such characters in their username field must enter their credentials exactly as they were at registration time in order to log in. We have no means of detecting such accounts on the server, as usernames are not visible to server admins. We'll consider this behaviour in the future if we introduce an option to change usernames as we do with passwords. +* We now double-check that login blocks (account credentials encrypted with a key derived from a username and password) can be accessed by the client when registering or changing passwords. It should be sufficient to rely on the server to report whether the encrypted credentials were stored successfully when uploading them, but in instances where these resources don't load due to a misbehaving browser extension it's better that we detect it at registration time rather than after the user creates content that will be difficult to access without assistance determining which extension or browser customization is to blame. +* We learned that the Javascript engine used on iOS has trouble parsing an alternative representation of data strings that every other platform seems to handle. This caused calendars to display incorrect data. Because Apple prevents third-party browsers from including their own JavaScript engines this means that users were affected by this Safari bug regardless of whether they used browsers branded as Safari, Firefox, Chrome, or otherwise. +* After some internal review we now guard against a variety of cases where user-crafted input could trigger a DOMException error and prevent a whole page worth of markdown content to fail to render. While there is no impact for users' privacy or security in this bug, a malicious user could exploit it to be annoying. +* Shortly after our last release a user reported being unable to access their account due to a typeError which we were able to [guard against](https://github.com/xwiki-labs/cryptpad/commit/abc9466abe71a76d1d31ef6a3c2c9bba4d2233e4). +* Images appearing in the 'lightbox' preview modal no longer appear stretched. +* Before applying actions that modify the team's membership we now confirm that server-enforced permissions match our local state. + # 4.7.0 ## Goals @@ -488,7 +547,7 @@ To upgrade from 3.24.0 to 3.25.0: ## Features * This release makes a lot of changes to how content is loaded over the network. - * Most notably, CryptPad now employs a client-side cache based on the the _indexedDB API_. Browsers that support this functionality will opportunistically store messages in a local cache for the next time they need them. This should make a considerable difference in how quickly you're able to load a pad, particularly if you accessing the server over a low-bandwidth network. + * Most notably, CryptPad now employs a client-side cache based on the _indexedDB API_. Browsers that support this functionality will opportunistically store messages in a local cache for the next time they need them. This should make a considerable difference in how quickly you're able to load a pad, particularly if you accessing the server over a low-bandwidth network. * Uploaded files (images, PDFs, etc.) are also cached in a similar way. Once you'd loaded an asset, your client will prefer to load its local copy instead of the server. * We've updated the code for our _full drive backup_ functionality so that it uses the local cache to load files more quickly. In addition to this, backing up the contents of your drive will also populate the cache as though you had loaded your documents in the normal fashion. This cache will persist until it is invalidated (due to the authoritative document having been deleted or had its history trimmed) or until you have logged out. * We've added the ability to configure the maximum size for automatically downloaded files. Any encrypted files that are above this size will instead require manual interaction to begin downloading. Files that are larger than this limit which are already loaded in your cache will still be automatically displayed. @@ -2008,7 +2067,7 @@ Finally, we prioritized the ability to archive files for a period instead of del * Users with existing friends on the platform will run a migration to allow them to share pads with friends directly instead of sending them a link. * they'll receive a notification indicating the title of the pad and who shared it * if you've already added friends on the platform, you can send them pads from the usual "sharing menu" -* Our code editor already offered the ability to set their color theme and highlighting mode, but now those values will be previewed when mousing over the the option in the dropdown. +* Our code editor already offered the ability to set their color theme and highlighting mode, but now those values will be previewed when mousing over the option in the dropdown. * Our slide editor now offers the same theme selection as the code editor * It's now possible to view the history of a shared folder by clicking the history button while viewing the shared folder's contents. diff --git a/config/config.example.js b/config/config.example.js index 69f0b1e91..96914fa92 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -276,4 +276,13 @@ module.exports = { * (false by default) */ verbose: false, + + /* Surplus information: + * + * 'installMethod' is included in server telemetry to voluntarily + * indicate how many instances are using unofficial installation methods + * such as Docker. + * + */ + installMethod: 'unspecified', }; diff --git a/customize.dist/login.js b/customize.dist/login.js index 74e5e86f6..3309190c0 100644 --- a/customize.dist/login.js +++ b/customize.dist/login.js @@ -153,7 +153,7 @@ define([ register: isRegister, }; - var RT, blockKeys, blockHash, Pinpad, rpc, userHash; + var RT, blockKeys, blockHash, blockUrl, Pinpad, rpc, userHash; nThen(function (waitFor) { // derive a predefined number of bytes from the user's inputs, @@ -171,7 +171,7 @@ define([ // the rest of their data // determine where a block for your set of keys would be stored - var blockUrl = Block.getBlockUrl(res.opt.blockKeys); + blockUrl = Block.getBlockUrl(res.opt.blockKeys); // Check whether there is a block at that location Util.fetch(blockUrl, waitFor(function (err, block) { @@ -412,12 +412,21 @@ define([ toPublish.edPublic = RT.proxy.edPublic; var blockRequest = Block.serialize(JSON.stringify(toPublish), res.opt.blockKeys); - rpc.writeLoginBlock(blockRequest, waitFor(function (e) { if (e) { console.error(e); + waitFor.abort(); return void cb(e); } + })); + }).nThen(function (waitFor) { + // confirm that the block was actually written before considering registration successful + Util.fetch(blockUrl, waitFor(function (err /*, block */) { + if (err) { + console.error(err); + waitFor.abort(); + return void cb(err); + } console.log("blockInfo available at:", blockHash); LocalStore.setBlockHash(blockHash); diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 977405db1..7d9af710a 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -9,6 +9,7 @@ var map = { 'fr': 'Français', //'hi': 'हिन्दी', 'it': 'Italiano', + 'ja': '日本語', 'nb': 'Norwegian Bokmål', //'pl': 'Polski', 'pt-br': 'Português do Brasil', diff --git a/customize.dist/pages.js b/customize.dist/pages.js index b0fd2f20a..18632f6ea 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -105,7 +105,7 @@ define([ var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ? '/imprint.html' : AppConfig.imprint); - Pages.versionString = "v4.7.0"; + Pages.versionString = "v4.8.0"; // used for the about menu diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index f8ea0c92b..5c1649850 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -53,7 +53,7 @@ @cryptpad_color_light_blue: #00b7d8; @cryptpad_color_red: #ff1100; @cryptpad_color_red_fade: fade(@cryptpad_color_red, 50%); -@cryptpad_color_red_fader: fade(@cryptpad_color_red, 25%); +@cryptpad_color_red_fader: fade(@cryptpad_color_red, 15%); @cryptpad_color_warn_red: @cryptpad_color_red_fade; @cryptpad_color_dark_red: #9e0000; @cryptpad_color_light_red: #FFD4D4; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index a7b103797..18dcb1c0a 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -53,7 +53,7 @@ @cryptpad_color_light_blue: #00b7d8; @cryptpad_color_red: #ff1100; @cryptpad_color_red_fade: fade(@cryptpad_color_red, 50%); -@cryptpad_color_red_fader: fade(@cryptpad_color_red, 25%); +@cryptpad_color_red_fader: fade(@cryptpad_color_red, 15%); @cryptpad_color_warn_red: @cryptpad_color_red_fade; @cryptpad_color_dark_red: #9e0000; @cryptpad_color_light_red: #FFD4D4; diff --git a/customize.dist/src/less2/include/markdown.less b/customize.dist/src/less2/include/markdown.less index 3943f128d..43732187a 100644 --- a/customize.dist/src/less2/include/markdown.less +++ b/customize.dist/src/less2/include/markdown.less @@ -154,6 +154,38 @@ color: @cp_markdown-block-fg; text-align: left; } + + div.cp-inline-img-warning { + display: inline-block; + padding: 10px; + color: @cryptpad_text_col; + background-color: @cryptpad_color_red_fader; + border: 1px solid @cryptpad_color_red; + .cp-inline-img { + display: flex; + margin-bottom: 10px; + } + .cp-alt-txt { + margin-left: 10px; + font-family: monospace; + font-size: 0.8em; + color: fade(@cryptpad_text_col, 90%); + } + a { + color: @cryptpad_text_col; + font-size: 0.8em; + &.cp-remote-img::before { // .fa.fa-question-circle + font-family: FontAwesome; + //content: "\f08e\00a0"; + content: "\f08e\00a0\00a0"; + } + &.cp-learn-more::before { // .fa.fa-external-link + font-family: FontAwesome; + content: "\f059\00a0"; + //content: "\f059\00a0\00a0"; + } + } + } } .markdown_cryptpad() { diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less index 92fef68d0..7ec19a699 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -221,6 +221,9 @@ button { line-height: 1.5; } + img { + align-self: center; + } & > iframe { width: 100%; height: 100%; diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index 85f42dd81..8bc47d9f8 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -64,7 +64,7 @@ server { add_header Permissions-Policy interest-cohort=(); set $coop ''; - if ($uri ~ ^\/(sheet|presentation|doc)\/.*$) { set $coop 'same-origin'; } + if ($uri ~ ^\/(sheet|presentation|doc|convert)\/.*$) { set $coop 'same-origin'; } # Enable SharedArrayBuffer in Firefox (for .xlsx export) add_header Cross-Origin-Resource-Policy cross-origin; @@ -214,7 +214,7 @@ server { # The nodejs server has some built-in forwarding rules to prevent # URLs like /pad from resulting in a 404. This simply adds a trailing slash # to a variety of applications. - location ~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams|calendar|presentation|doc|form|report)$ { + location ~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams|calendar|presentation|doc|form|report|convert)$ { rewrite ^(.*)$ $1/ redirect; } diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index ab48a1086..59750571e 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -326,6 +326,7 @@ var instanceStatus = function (Env, Server, cb) { blockDailyCheck: Env.blockDailyCheck, updateAvailable: Env.updateAvailable, + instancePurpose: Env.instancePurpose, }); }; diff --git a/lib/commands/block.js b/lib/commands/block.js index 8180cb68e..4af32af70 100644 --- a/lib/commands/block.js +++ b/lib/commands/block.js @@ -1,14 +1,10 @@ /*jshint esversion: 6 */ /* globals Buffer*/ -var Block = module.exports; - -const Fs = require("fs"); -const Fse = require("fs-extra"); -const Path = require("path"); +const Block = module.exports; const Nacl = require("tweetnacl/nacl-fast"); const nThen = require("nthen"); - const Util = require("../common-util"); +const BlockStore = require("../storage/block"); /* We assume that the server is secured against MitM attacks @@ -31,7 +27,9 @@ const Util = require("../common-util"); author of the block, since we assume that the block will have been encrypted with xsalsa20-poly1305 which is authenticated. */ -var validateLoginBlock = function (Env, publicKey, signature, block, cb) { // FIXME BLOCKS +Block.validateLoginBlock = function (Env, publicKey, signature, block, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + // convert the public key to a Uint8Array and validate it if (typeof(publicKey) !== 'string') { return void cb('E_INVALID_KEY'); } @@ -69,21 +67,7 @@ var validateLoginBlock = function (Env, publicKey, signature, block, cb) { // FI // call back with (err) if unsuccessful if (!verified) { return void cb("E_COULD_NOT_VERIFY"); } - return void cb(null, u8_block); -}; - -var createLoginBlockPath = function (Env, publicKey) { // FIXME BLOCKS - // prepare publicKey to be used as a file name - var safeKey = Util.escapeKeyCharacters(publicKey); - - // validate safeKey - if (typeof(safeKey) !== 'string') { - return; - } - - // derive the full path - // /home/cryptpad/cryptpad/block/fg/fg32kefksjdgjkewrjksdfksjdfsdfskdjfsfd - return Path.join(Env.paths.block, safeKey.slice(0, 2), safeKey); + return void cb(null, block); }; Block.validateAncestorProof = function (Env, proof, _cb) { @@ -103,31 +87,26 @@ Block.validateAncestorProof = function (Env, proof, _cb) { return void cb('E_INVALID_ANCESTOR_PROOF'); } // else fall through to next step - }).nThen(function (w) { - var path = createLoginBlockPath(Env, pub); - Fs.access(path, Fs.constants.F_OK, w(function (err) { - if (!err) { return; } - w.abort(); // else - return void cb("E_MISSING_ANCESTOR"); - })); }).nThen(function () { - cb(void 0, pub); + BlockStore.check(Env, pub, function (err) { + if (err) { return void cb('E_MISSING_ANCESTOR'); } + cb(void 0, pub); + }); }); } catch (err) { return void cb(err); } }; -Block.writeLoginBlock = function (Env, safeKey, msg, _cb) { // FIXME BLOCKS +Block.writeLoginBlock = function (Env, safeKey, msg, _cb) { var cb = Util.once(Util.mkAsync(_cb)); - //console.log(msg); var publicKey = msg[0]; var signature = msg[1]; var block = msg[2]; var registrationProof = msg[3]; var previousKey; - var validatedBlock, parsed, path; + var validatedBlock, path; nThen(function (w) { if (Util.escapeKeyCharacters(publicKey) !== safeKey) { w.abort(); @@ -156,44 +135,26 @@ Block.writeLoginBlock = function (Env, safeKey, msg, _cb) { // FIXME BLOCKS previousKey = provenKey; })); }).nThen(function (w) { - validateLoginBlock(Env, publicKey, signature, block, w(function (e, _validatedBlock) { + Env.validateLoginBlock(publicKey, signature, block, w(function (e, _validatedBlock) { if (e) { w.abort(); return void cb(e); } - if (!(_validatedBlock instanceof Uint8Array)) { + if (typeof(_validatedBlock) !== 'string') { w.abort(); - return void cb('E_INVALID_BLOCK'); + return void cb('E_INVALID_BLOCK_RETURNED'); } validatedBlock = _validatedBlock; - - // derive the filepath - path = createLoginBlockPath(Env, publicKey); - - // make sure the path is valid - if (typeof(path) !== 'string') { - return void cb('E_INVALID_BLOCK_PATH'); - } - - parsed = Path.parse(path); - if (!parsed || typeof(parsed.dir) !== 'string') { - w.abort(); - return void cb("E_INVALID_BLOCK_PATH_2"); - } - })); - }).nThen(function (w) { - // make sure the path to the file exists - Fse.mkdirp(parsed.dir, w(function (e) { - if (e) { - w.abort(); - cb(e); - } })); }).nThen(function () { - // actually write the block - Fs.writeFile(path, Buffer.from(validatedBlock), { encoding: "binary", }, function (err) { - if (err) { return void cb(err); } + var buffer; + try { + buffer = Buffer.from(Nacl.util.decodeBase64(validatedBlock)); + } catch (err) { + return void cb('E_BLOCK_DESERIALIZATION'); + } + BlockStore.write(Env, publicKey, buffer, function (err) { Env.Log.info('BLOCK_WRITE_BY_OWNER', { safeKey: safeKey, blockId: publicKey, @@ -201,11 +162,13 @@ Block.writeLoginBlock = function (Env, safeKey, msg, _cb) { // FIXME BLOCKS previousKey: previousKey, path: path, }); - cb(); + cb(err); }); }); }; +const DELETE_BLOCK = Nacl.util.encodeBase64(Nacl.util.decodeUTF8('DELETE_BLOCK')); + /* When users write a block, they upload the block, and provide a signature proving that they deserve to be able to write to @@ -216,10 +179,11 @@ Block.writeLoginBlock = function (Env, safeKey, msg, _cb) { // FIXME BLOCKS information, we can just sign some constant and use that as proof. */ -Block.removeLoginBlock = function (Env, safeKey, msg, cb) { // FIXME BLOCKS +Block.removeLoginBlock = function (Env, safeKey, msg, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + var publicKey = msg[0]; var signature = msg[1]; - var block = Nacl.util.decodeUTF8('DELETE_BLOCK'); // clients and the server will have to agree on this constant nThen(function (w) { if (Util.escapeKeyCharacters(publicKey) !== safeKey) { @@ -227,26 +191,14 @@ Block.removeLoginBlock = function (Env, safeKey, msg, cb) { // FIXME BLOCKS return void cb("INCORRECT_KEY"); } }).nThen(function () { - validateLoginBlock(Env, publicKey, signature, block, function (e /*::, validatedBlock */) { + Env.validateLoginBlock(publicKey, signature, DELETE_BLOCK, function (e) { if (e) { return void cb(e); } - // derive the filepath - var path = createLoginBlockPath(Env, publicKey); - - // make sure the path is valid - if (typeof(path) !== 'string') { - return void cb('E_INVALID_BLOCK_PATH'); - } - - // FIXME COLDSTORAGE - Fs.unlink(path, function (err) { - Env.Log.info('DELETION_BLOCK_BY_OWNER_RPC', { + BlockStore.archive(Env, publicKey, function (err) { + Env.Log.info('ARCHIVAL_BLOCK_BY_OWNER_RPC', { publicKey: publicKey, - path: path, status: err? String(err): 'SUCCESS', }); - - if (err) { return void cb(err); } - cb(); + cb(err); }); }); }); diff --git a/lib/commands/pin-rpc.js b/lib/commands/pin-rpc.js index 93d922620..89afc4ee8 100644 --- a/lib/commands/pin-rpc.js +++ b/lib/commands/pin-rpc.js @@ -8,7 +8,7 @@ const nThen = require("nthen"); //const escapeKeyCharacters = Util.escapeKeyCharacters; const unescapeKeyCharacters = Util.unescapeKeyCharacters; -var sumChannelSizes = function (sizes) { +var sumChannelSizes = function (sizes) { // FIXME this synchronous code could be done by a worker return Object.keys(sizes).map(function (id) { return sizes[id]; }) .filter(function (x) { // only allow positive numbers @@ -115,8 +115,8 @@ Pinning.getTotalSize = function (Env, safeKey, cb) { */ Pinning.removePins = function (Env, safeKey, cb) { // FIXME respect the queue - Env.pinStore.removeChannel(safeKey, function (err) { - Env.Log.info('DELETION_PIN_BY_OWNER_RPC', { + Env.pinStore.archiveChannel(safeKey, function (err) { + Env.Log.info('ARCHIVAL_PIN_BY_OWNER_RPC', { safeKey: safeKey, status: err? String(err): 'SUCCESS', }); @@ -145,7 +145,7 @@ var getFreeSpace = Pinning.getFreeSpace = function (Env, safeKey, cb) { }); }; -var getHash = Pinning.getHash = function (Env, safeKey, cb) { +Pinning.getHash = function (Env, safeKey, cb) { getChannelList(Env, safeKey, function (channels) { Env.hashChannelList(channels, cb); }); @@ -166,12 +166,12 @@ Pinning.pinChannel = function (Env, safeKey, channels, cb) { }); if (toStore.length === 0) { - return void getHash(Env, safeKey, cb); + return void cb(); } getMultipleFileSize(Env, toStore, function (e, sizes) { if (typeof(sizes) === 'undefined') { return void cb(e); } - var pinSize = sumChannelSizes(sizes); + var pinSize = sumChannelSizes(sizes); // FIXME don't do this in the main thread... getFreeSpace(Env, safeKey, function (e, free) { if (typeof(free) === 'undefined') { @@ -186,7 +186,7 @@ Pinning.pinChannel = function (Env, safeKey, channels, cb) { toStore.forEach(function (channel) { session.channels[channel] = true; }); - getHash(Env, safeKey, cb); + cb(); }); }); }); @@ -208,7 +208,7 @@ Pinning.unpinChannel = function (Env, safeKey, channels, cb) { }); if (toStore.length === 0) { - return void getHash(Env, safeKey, cb); + return void cb(); } Env.pinStore.message(safeKey, JSON.stringify(['UNPIN', toStore, +new Date()]), @@ -217,20 +217,19 @@ Pinning.unpinChannel = function (Env, safeKey, channels, cb) { toStore.forEach(function (channel) { delete session.channels[channel]; }); - getHash(Env, safeKey, cb); + cb(); }); }); }; -Pinning.resetUserPins = function (Env, safeKey, channelList, cb) { +Pinning.resetUserPins = function (Env, safeKey, channelList, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); if (!Array.isArray(channelList)) { return void cb('INVALID_PIN_LIST'); } var session = Core.getSession(Env.Sessions, safeKey); + if (!channelList.length) { - return void getHash(Env, safeKey, function (e, hash) { - if (e) { return cb(e); } - cb(void 0, hash); - }); + return void cb(); } var pins = {}; @@ -270,9 +269,7 @@ Pinning.resetUserPins = function (Env, safeKey, channelList, cb) { // update in-memory cache IFF the reset was allowed. session.channels = pins; - getHash(Env, safeKey, function (e, hash) { - cb(e, hash); - }); + cb(); }); }); }); diff --git a/lib/decrees.js b/lib/decrees.js index 960ece1ef..5f599705e 100644 --- a/lib/decrees.js +++ b/lib/decrees.js @@ -173,6 +173,9 @@ commands.SET_SUPPORT_MAILBOX = makeGenericSetter('supportMailbox', function (arg return args_isString(args) && Core.isValidPublicKey(args[0]); }); +// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_PURPOSE', ["development"]]], console.log) +commands.SET_INSTANCE_PURPOSE = makeGenericSetter('instancePurpose', args_isString); + // Maintenance: Empty string or an object with a start and end time var isNumber = function (value) { return typeof(value) === "number" && !isNaN(value); diff --git a/lib/env.js b/lib/env.js index 6f1717c09..3d58f11f7 100644 --- a/lib/env.js +++ b/lib/env.js @@ -20,6 +20,7 @@ var canonicalizeOrigin = function (s) { module.exports.create = function (config) { const Env = { version: Package.version, + installMethod: config.installMethod || undefined, httpUnsafeOrigin: canonicalizeOrigin(config.httpUnsafeOrigin), httpSafeOrigin: canonicalizeOrigin(config.httpSafeOrigin), @@ -122,7 +123,7 @@ module.exports.create = function (config) { maxWorkers: config.maxWorkers, disableIntegratedTasks: config.disableIntegratedTasks || false, - disableIntegratedEviction: config.disableIntegratedEviction || false, + disableIntegratedEviction: typeof(config.disableIntegratedEviction) === 'undefined'? true: config.disableIntegratedEviction, // XXX false, lastEviction: +new Date(), evictionReport: {}, commandTimers: {}, diff --git a/lib/historyKeeper.js b/lib/historyKeeper.js index daa041b5f..44afe0ae0 100644 --- a/lib/historyKeeper.js +++ b/lib/historyKeeper.js @@ -123,6 +123,8 @@ module.exports.create = function (Env, cb) { Store.create({ filePath: pinPath, archivePath: Env.paths.archive, + // indicate that archives should be put in a 'pins' archvie folder + volumeId: 'pins', }, w(function (err, s) { if (err) { throw err; } Env.pinStore = s; diff --git a/lib/stats.js b/lib/stats.js index d1da0e202..1dbbb2ad9 100644 --- a/lib/stats.js +++ b/lib/stats.js @@ -4,6 +4,7 @@ const Stats = module.exports; Stats.instanceData = function (Env) { var data = { version: Env.version, + installMethod: Env.installMethod, domain: Env.myDomain, subdomain: Env.mySubdomain, @@ -11,8 +12,10 @@ Stats.instanceData = function (Env) { httpUnsafeOrigin: Env.httpUnsafeOrigin, httpSafeOrigin: Env.httpSafeOrigin, - adminEmail: Env.adminEmail, + adminEmail: Env.consentToContact? Env.adminEmail: undefined, consentToContact: Boolean(Env.consentToContact), + + instancePurpose: Env.instancePurpose === 'noanswer'? undefined: Env.instancePurpose, }; /* We reserve the right to choose not to include instances diff --git a/lib/storage/block.js b/lib/storage/block.js new file mode 100644 index 000000000..1078f6d2e --- /dev/null +++ b/lib/storage/block.js @@ -0,0 +1,88 @@ +/*jshint esversion: 6 */ +const Block = module.exports; +const Util = require("../common-util"); +const Path = require("path"); +const Fs = require("fs"); +const Fse = require("fs-extra"); +const nThen = require("nthen"); + +Block.mkPath = function (Env, publicKey) { + // prepare publicKey to be used as a file name + var safeKey = Util.escapeKeyCharacters(publicKey); + + // validate safeKey + if (typeof(safeKey) !== 'string') { return; } + + // derive the full path + // /home/cryptpad/cryptpad/block/fg/fg32kefksjdgjkewrjksdfksjdfsdfskdjfsfd + return Path.join(Env.paths.block, safeKey.slice(0, 2), safeKey); +}; + +Block.mkArchivePath = function (Env, publicKey) { + // prepare publicKey to be used as a file name + var safeKey = Util.escapeKeyCharacters(publicKey); + + // validate safeKey + if (typeof(safeKey) !== 'string') { + return; + } + + // derive the full path + // /home/cryptpad/cryptpad/block/fg/fg32kefksjdgjkewrjksdfksjdfsdfskdjfsfd + return Path.join(Env.paths.archive, 'block', safeKey.slice(0, 2), safeKey); +}; + +Block.archive = function (Env, publicKey, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + + // derive the filepath + var currentPath = Block.mkPath(Env, publicKey); + + // make sure the path is valid + if (typeof(currentPath) !== 'string') { + return void cb('E_INVALID_BLOCK_PATH'); + } + + var archivePath = Block.mkArchivePath(Env, publicKey); + // make sure the path is valid + if (typeof(archivePath) !== 'string') { + return void cb('E_INVALID_BLOCK_ARCHIVAL_PATH'); + } + + Fse.move(currentPath, archivePath, { + overwrite: true, + }, cb); +}; + +Block.check = function (Env, publicKey, _cb) { // 'check' because 'exists' implies boolean + var cb = Util.once(Util.mkAsync(_cb)); + var path = Block.mkPath(Env, publicKey); + Fs.access(path, Fs.constants.F_OK, cb); +}; + +Block.write = function (Env, publicKey, buffer, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + var path = Block.mkPath(Env, publicKey); + if (typeof(path) !== 'string') { return void cb('INVALID_PATH'); } + var parsed = Path.parse(path); + + nThen(function (w) { + Fse.mkdirp(parsed.dir, w(function (err) { + if (!err) { return; } + w.abort(); + cb(err); + })); + }).nThen(function (w) { + Block.archive(Env, publicKey, w(function (/* err */) { + /* + we proceed even if there are errors. + it might be ENOENT (there is no file to archive) + or EACCES (bad filesystem permissions for the existing archived block?) + or lots of other things, none of which justify preventing the write + */ + })); + }).nThen(function () { + Fs.writeFile(path, buffer, { encoding: 'binary' }, cb); + }); +}; + diff --git a/lib/storage/file.js b/lib/storage/file.js index 825f14066..30e6c7644 100644 --- a/lib/storage/file.js +++ b/lib/storage/file.js @@ -51,7 +51,7 @@ var mkPath = function (env, channelId) { }; var mkArchivePath = function (env, channelId) { - return Path.join(env.archiveRoot, 'datastore', channelId.slice(0, 2), channelId) + '.ndjson'; + return Path.join(env.archiveRoot, env.volumeId, channelId.slice(0, 2), channelId) + '.ndjson'; }; var mkMetadataPath = function (env, channelId) { @@ -59,7 +59,7 @@ var mkMetadataPath = function (env, channelId) { }; var mkArchiveMetadataPath = function (env, channelId) { - return Path.join(env.archiveRoot, 'datastore', channelId.slice(0, 2), channelId) + '.metadata.ndjson'; + return Path.join(env.archiveRoot, env.volumeId, channelId.slice(0, 2), channelId) + '.metadata.ndjson'; }; var mkTempPath = function (env, channelId) { @@ -1044,6 +1044,9 @@ module.exports.create = function (conf, _cb) { var env = { root: conf.filePath || './datastore', archiveRoot: conf.archivePath || './data/archive', + // supply a volumeId if you want a store to archive channels to and from + // to its own subpath within the archive directory + volumeId: conf.volumeId || 'datastore', channels: { }, batchGetChannel: BatchRead('store_batch_channel'), }; @@ -1076,7 +1079,7 @@ module.exports.create = function (conf, _cb) { } })); // make sure the cold storage directory exists - Fse.mkdirp(env.archiveRoot, PERMISSIVE, w(function (err) { + Fse.mkdirp(Path.join(env.archiveRoot, env.volumeId), PERMISSIVE, w(function (err) { if (err && err.code !== 'EEXIST') { w.abort(); return void cb(err); diff --git a/lib/workers/db-worker.js b/lib/workers/db-worker.js index 8585cc3f6..2371240ec 100644 --- a/lib/workers/db-worker.js +++ b/lib/workers/db-worker.js @@ -66,6 +66,9 @@ const init = function (config, _cb) { Store.create({ filePath: config.pinPath, archivePath: config.archivePath, + // important to initialize the pinstore with its own volume id + // otherwise archived pin logs will get mixed in with channels + volumeId: 'pins', }, w(function (err, _pinStore) { if (err) { w.abort(); @@ -694,6 +697,10 @@ COMMANDS.VALIDATE_ANCESTOR_PROOF = function (data, cb) { Block.validateAncestorProof(Env, data && data.proof, cb); }; +COMMANDS.VALIDATE_LOGIN_BLOCK = function (data, cb) { + Block.validateLoginBlock(Env, data.publicKey, data.signature, data.block, cb); +}; + process.on('message', function (data) { if (!data || !data.txid || !data.pid) { return void process.send({ diff --git a/lib/workers/index.js b/lib/workers/index.js index 85c66eeb5..c2bfb5740 100644 --- a/lib/workers/index.js +++ b/lib/workers/index.js @@ -451,6 +451,15 @@ Workers.initialize = function (Env, config, _cb) { }, cb); }; + Env.validateLoginBlock = function (publicKey, signature, block, cb) { + sendCommand({ + command: 'VALIDATE_LOGIN_BLOCK', + publicKey: publicKey, + signature: signature, + block: block, + }, cb); + }; + cb(void 0); }); }; diff --git a/package-lock.json b/package-lock.json index ab294ec92..4d7992f82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "4.7.0", + "version": "4.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f9dfc2c14..9e18f05f0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "4.7.0", + "version": "4.8.0", "license": "AGPL-3.0+", "repository": { "type": "git", diff --git a/scripts/evict-archived.js b/scripts/evict-archived.js index 7f90f9ff5..255246e99 100644 --- a/scripts/evict-archived.js +++ b/scripts/evict-archived.js @@ -56,6 +56,8 @@ var prepareEnv = function (Env, cb) { Store.create({ filePath: config.pinPath, + // archive pin logs to their own subpath + volumeId: 'pins', }, w(function (err, _) { if (err) { w.abort(); diff --git a/scripts/evict-inactive.js b/scripts/evict-inactive.js index bf7e1ca5b..2521e014f 100644 --- a/scripts/evict-inactive.js +++ b/scripts/evict-inactive.js @@ -56,6 +56,8 @@ var prepareEnv = function (Env, cb) { Store.create({ filePath: config.pinPath, + // archive pin logs to their own subpath + volumeId: 'pins', }, w(function (err, _) { if (err) { w.abort(); diff --git a/scripts/find-html-translations.js b/scripts/find-html-translations.js index dabdbac4b..fc1d4a18e 100644 --- a/scripts/find-html-translations.js +++ b/scripts/find-html-translations.js @@ -9,6 +9,7 @@ var simpleTags = [ // FIXME "", + '', '

', '

', @@ -70,7 +71,7 @@ processLang(EN, 'en', true); [ 'ar', - 'bn_BD', + //'bn_BD', 'ca', 'de', 'es', @@ -86,11 +87,15 @@ processLang(EN, 'en', true); 'ro', 'ru', 'sv', - 'te', + //'te', 'tr', 'zh', ].forEach(function (lang) { - var map = require("../www/common/translations/messages." + lang + ".json"); - if (!Object.keys(map).length) { return; } - processLang(map, lang); + try { + var map = require("../www/common/translations/messages." + lang + ".json"); + if (!Object.keys(map).length) { return; } + processLang(map, lang); + } catch (err) { + console.error(err); + } }); diff --git a/scripts/lint-translations.js b/scripts/lint-translations.js new file mode 100644 index 000000000..a38cd615a --- /dev/null +++ b/scripts/lint-translations.js @@ -0,0 +1,8 @@ +// TODO unify the following scripts + // unused-translations.js + // find-html-translations + +// more linting + // Search for 'Cryptpad' string (should be 'CryptPad') + // Search English for -ise\s + diff --git a/scripts/tests/test-rpc.js b/scripts/tests/test-rpc.js index 0fd0abc37..b92cf6287 100644 --- a/scripts/tests/test-rpc.js +++ b/scripts/tests/test-rpc.js @@ -122,14 +122,11 @@ var createUser = function (config, cb) { }); })); }).nThen(function (w) { - user.rpc.reset([], w(function (err, hash) { + user.rpc.reset([], w(function (err) { if (err) { w.abort(); user.shutdown(); - return console.log("RESET_ERR"); - } - if (!hash || hash !== EMPTY_ARRAY_HASH) { - throw new Error("EXPECTED EMPTY ARRAY HASH"); + return console.log("TEST_RESET_ERR"); } })); }).nThen(function (w) { @@ -214,17 +211,17 @@ var createUser = function (config, cb) { // TODO check your quota usage }).nThen(function (w) { - user.rpc.unpin([user.mailboxChannel], w(function (err, hash) { + user.rpc.unpin([user.mailboxChannel], w(function (err) { if (err) { w.abort(); return void cb(err); } + })); + }).nThen(function (w) { + user.rpc.getServerHash(w(function (err, hash) { + console.log(hash); - if (hash[0] !== EMPTY_ARRAY_HASH) { - //console.log('UNPIN_RESPONSE', hash); - throw new Error("UNPIN_DIDNT_WORK"); - } - user.latestPinHash = hash[0]; + user.latestPinHash = hash; })); }).nThen(function (w) { // clean up the pin list to avoid lots of accounts on the server @@ -304,7 +301,8 @@ nThen(function (w) { }, w(function (err, roster) { if (err) { w.abort(); - return void console.trace(err); + console.error(err); + return void console.error("ROSTER_ERROR"); } oscar.roster = roster; oscar.destroy.reg(function () { diff --git a/server.js b/server.js index 848167a98..3cea171c6 100644 --- a/server.js +++ b/server.js @@ -90,7 +90,7 @@ var setHeaders = (function () { return function (req, res) { // apply a bunch of cross-origin headers for XLSX export in FF and printing elsewhere applyHeaderMap(res, { - "Cross-Origin-Opener-Policy": /^\/sheet\//.test(req.url)? 'same-origin': '', + "Cross-Origin-Opener-Policy": /^\/(sheet|presentation|doc|convert)\//.test(req.url)? 'same-origin': '', }); if (Env.NO_SANDBOX) { // handles correct configuration for local development diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less index 17450361c..227792540 100644 --- a/www/admin/app-admin.less +++ b/www/admin/app-admin.less @@ -203,6 +203,17 @@ } } + .cp-admin-radio-container { + display: flex; + align-items: left; //center; + flex-wrap: wrap; + flex-direction: column; + label { + margin-right: 40px; + margin-top: 5px; + } + } + .cp-admin-broadcast-form { input.flatpickr-input { width: 307.875px !important; // same width as flatpickr calendar diff --git a/www/admin/inner.js b/www/admin/inner.js index 30d0ec1a6..61cf05896 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -94,6 +94,7 @@ define([ 'cp-admin-list-my-instance', 'cp-admin-consent-to-contact', 'cp-admin-remove-donate-button', + 'cp-admin-instance-purpose', ], }; @@ -1853,6 +1854,73 @@ define([ }, }); + var sendDecree = function (data, cb) { + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: data, + }, cb); + }; + + create['instance-purpose'] = function () { + var key = 'instance-purpose'; + var $div = makeBlock(key); // Messages.admin_instancePurposeTitle.admin_instancePurposeHint + + var values = [ + 'noanswer', // Messages.admin_purpose_noanswer + 'experiment', // Messages.admin_purpose_experiment + 'personal', // Messages.admin_purpose_personal + 'education', // Messages.admin_purpose_education + 'org', // Messages.admin_purpose_org + 'business', // Messages.admin_purpose_business + 'public', // Messages.admin_purpose_public + ]; + + var defaultPurpose = 'noanswer'; + var purpose = APP.instanceStatus.instancePurpose || defaultPurpose; + + var opts = h('div.cp-admin-radio-container', [ + values.map(function (key) { + var full_key = 'admin_purpose_' + key; + return UI.createRadio('cp-instance-purpose-radio', 'cp-instance-purpose-radio-'+key, + Messages[full_key] || Messages._getKey(full_key, [defaultPurpose]), + key === purpose, { + input: { value: key }, + label: { class: 'noTitle' } + }); + }) + ]); + + var $opts = $(opts); + //var $br = $(h('br',)); + //$div.append($br); + + $div.append(opts); + + var setPurpose = function (value, cb) { + sendDecree([ + 'SET_INSTANCE_PURPOSE', + [ value] + ], cb); + }; + + $opts.on('change', function () { + var val = $opts.find('input:radio:checked').val(); + console.log(val); + //spinner.spin(); + setPurpose(val, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + //spinner.hide(); + return; + } + //spinner.done(); + UI.log(Messages.saved); + }); + }); + + return $div; + }; + var hideCategories = function () { APP.$rightside.find('> div').hide(); }; diff --git a/www/checkup/main.js b/www/checkup/main.js index 925cd4b7b..f4a40d11e 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -732,6 +732,84 @@ define([ cb(isHTTPS(trimmedUnsafe) && isHTTPS(trimmedSafe)); }); + [ + 'sheet', + 'presentation', + 'doc', + 'convert', + ].forEach(function (url) { + assert(function (cb, msg) { + var header = 'cross-origin-opener-policy'; + var expected = 'same-origin'; + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: '/' + url + '/', + header: header, + } + }, function (content) { + msg.appendChild(h('span', [ + code(url), + ' was served without the correct ', + code(header), + ' HTTP header value (', + code(expected), + '). This will interfere with your ability to convert between office file formats.' + ])); + cb(content === expected); + }); + }); + }); + +/* + assert(function (cb, msg) { + setWarningClass(msg); + $.ajax(cacheBuster('/'), { + dataType: 'text', + complete: function (xhr) { + var serverToken = xhr.getResponseHeader('server'); + if (serverToken === null) { return void cb(true); } + + var lowered = (serverToken || '').toLowerCase(); + var family; + + ['Apache', 'Caddy', 'NGINX'].some(function (pattern) { + if (lowered.indexOf(pattern.toLowerCase()) !== -1) { + family = pattern; + return true; + } + }); + + var text = [ + "This instance is set to respond with an HTTP ", + code("server"), + " header. This information can make it easier for attackers to find and exploit known vulnerabilities. ", + ]; + + if (family === 'NGINX') { // FIXME incorrect instructions for HTTP2. needs a recompile? + msg.appendChild(h('span', text.concat([ + "This can be addressed by setting ", + code("server_tokens off"), + " in your global NGINX config." + ]))); + return void cb(serverToken); + } + + // handle other + msg.appendChild(h('span', text.concat([ + "In this case, it appears that the host server is running ", + code(serverToken), + " instead of ", + code("NGINX"), + " as recommended. As such, you may not benefit from the latest security enhancements that are tested and maintained by the CryptPad development team.", + ]))); + + cb(serverToken); + } + }); + }); +*/ + if (false) { assert(function (cb, msg) { msg.innerText = 'fake test to simulate failure'; diff --git a/www/checkup/sandbox/main.js b/www/checkup/sandbox/main.js index 7ddfb07f8..e11aa1d52 100644 --- a/www/checkup/sandbox/main.js +++ b/www/checkup/sandbox/main.js @@ -27,12 +27,14 @@ define([ }; window.addEventListener("message", function (event) { + var txid, command; if (event && event.data) { try { //console.log(JSON.parse(event.data)); var msg = JSON.parse(event.data); - var command = msg.command; - var txid = msg.txid; + command = msg.command; + txid = msg.txid; + if (!txid) { return; } COMMANDS[command](msg.content, function (response) { // postMessage with same txid postMessage({ @@ -41,7 +43,11 @@ define([ }); }); } catch (err) { - console.error(err); + postMessage({ + txid: txid, + content: err, + }); + console.error(err, command); } } else { console.error(event); diff --git a/www/code/inner.js b/www/code/inner.js index d74513035..785e82788 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -507,7 +507,7 @@ define([ var fileHost = privateData.fileHost || privateData.origin; var src = fileHost + Hash.getBlobPathFromHex(secret.channel); var key = Hash.encodeBase64(secret.keys.cryptKey); - var mt = ''; + var mt = UI.mediaTag(src, key).outerHTML; editor.replaceSelection(mt); } }; diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index ad5f726f8..15e5b5e34 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -12,7 +12,7 @@ define(function() { * You should never remove the drive from this list. */ AppConfig.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard', - /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts', 'form']; + /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts', 'form', 'convert']; /* 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. @@ -22,6 +22,12 @@ define(function() { */ AppConfig.registeredOnlyTypes = ['file', 'contacts', 'notifications', 'support']; + // to prevent apps that aren't officially supported from showing up + // in the document creation modal + AppConfig.hiddenTypes = ['drive', 'teams', 'contacts', 'todo', 'file', 'accounts', 'calendar', 'poll', 'convert', + //'doc', 'presentation' + ]; + /* 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 * will use the english version instead. You can customize the langauges you want to be available diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 9bdf9ec7a..4d3520426 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -41,6 +41,15 @@ define([ return e; }; + // FIXME almost everywhere this is used would also be + // a good candidate for sframe-common's getMediatagFromHref + UI.mediaTag = function (src, key) { + return h('media-tag', { + src: src, + 'data-crypto-key': 'cryptpad:' + key, + }); + }; + var findCancelButton = UI.findCancelButton = function (root) { if (root) { return $(root).find('button.cancel').last(); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 3e27b4ee7..0ab010cb5 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1886,8 +1886,11 @@ define([ }, content: h('span', Messages.logoutEverywhere), action: function () { - Common.getSframeChannel().query('Q_LOGOUT_EVERYWHERE', null, function () { - Common.gotoURL(origin + '/'); + UI.confirm(Messages.settings_logoutEverywhereConfirm, function (yes) { + if (!yes) { return; } + Common.getSframeChannel().query('Q_LOGOUT_EVERYWHERE', null, function () { + Common.gotoURL(origin + '/'); + }); }); }, }); @@ -2075,15 +2078,9 @@ define([ var $container = $('
'); var i = 0; + var types = AppConfig.availablePadTypes.filter(function (p) { - if (p === 'drive') { return; } - if (p === 'teams') { return; } - if (p === 'contacts') { return; } - if (p === 'todo') { return; } - if (p === 'file') { return; } - if (p === 'accounts') { return; } - if (p === 'calendar') { return; } - if (p === 'poll') { return; } // Replaced by forms + if (AppConfig.hiddenTypes.indexOf(p) !== -1) { return; } if (!common.isLoggedIn() && AppConfig.registeredOnlyTypes && AppConfig.registeredOnlyTypes.indexOf(p) !== -1) { return; } return true; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 9eb5398e7..15234c35e 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -1937,6 +1937,17 @@ define([ waitFor.abort(); return void cb(obj); } + })); + }).nThen(function (waitFor) { + var blockUrl = Block.getBlockUrl(blockKeys); + Util.fetch(blockUrl, waitFor(function (err /* block */) { + if (err) { + console.error(err); + waitFor.abort(); + return cb({ + error: err, + }); + } console.log("new login block written"); var newBlockHash = Block.getBlockHash(blockKeys); LocalStore.setBlockHash(newBlockHash); diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index cb95f37aa..0fe8aed0a 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -8,11 +8,14 @@ define([ '/common/inner/common-mediatag.js', '/common/media-tag.js', '/customize/messages.js', + '/common/less.min.js', + '/customize/pages.js', + '/common/highlight/highlight.pack.js', '/lib/diff-dom/diffDOM.js', '/bower_components/tweetnacl/nacl-fast.min.js', 'css!/common/highlight/styles/'+ (window.CryptPad_theme === 'dark' ? 'dark.css' : 'github.css') -],function ($, ApiConfig, Marked, Hash, Util, h, MT, MediaTag, Messages) { +],function ($, ApiConfig, Marked, Hash, Util, h, MT, MediaTag, Messages, Less, Pages) { var DiffMd = {}; var Highlight = window.hljs; @@ -172,12 +175,16 @@ define([ return h('div.cp-md-toc', content).outerHTML; }; - DiffMd.render = function (md, sanitize, restrictedMd) { + var noHeadingId = false; + DiffMd.render = function (md, sanitize, restrictedMd, noId) { Marked.setOptions({ renderer: restrictedMd ? restrictedRenderer : renderer, }); + noHeadingId = noId; var r = Marked(md, { - sanitize: sanitize + sanitize: sanitize, + headerIds: !noId, + gfm: true, }); // Add Table of Content @@ -207,7 +214,11 @@ define([ }; restrictedRenderer.code = renderer.code; + var _heading = renderer.heading; renderer.heading = function (text, level) { + if (noHeadingId) { + return _heading.apply(this, arguments); + } var i = 0; var safeText = text.toLowerCase().replace(/[^\w]+/g, '-'); var getId = function () { @@ -266,9 +277,43 @@ define([ return '
  • ' + text + '
  • \n'; }; + + var qualifiedHref = function (href) { + if (typeof(window.URL) === 'undefined') { return href; } + try { + var url = new URL(href, ApiConfig.httpUnsafeOrigin); + return url.href; + } catch (err) { + console.error(err); + return href; + } + }; + + var isLocalURL = function (href) { + // treat all URLs as remote if you are using an ancient browser + if (typeof(window.URL) === 'undefined') { return false; } + try { + var url = new URL(href, ApiConfig.httpUnsafeOrigin); + // FIXME data URLs can be quite large, but that should be addressed + // in the source markdown's, not the renderer + if (url.protocol === 'data:') { return true; } + var localURL = new URL(ApiConfig.httpUnsafeOrigin); + return url.host === localURL.host; + } catch (err) { + return true; + } + }; + renderer.image = function (href, title, text) { + if (isLocalURL(href) && href.slice(0, 6) !== '/file/') { + return h('img', { + src: href, + title: title || '', + alt: text, + }).outerHTML; + } + if (href.slice(0,6) === '/file/') { - // DEPRECATED // Mediatag using markdown syntax should not be used anymore so they don't support // password-protected files console.log('DEPRECATED: mediatag using markdown syntax!'); @@ -276,19 +321,38 @@ define([ var secret = Hash.getSecrets('file', parsed.hash); var src = (ApiConfig.fileHost || '') +Hash.getBlobPathFromHex(secret.channel); var key = Hash.encodeBase64(secret.keys.cryptKey); - var mt = ''; - if (mediaMap[src]) { - mt += mediaMap[src]; - } - mt += ''; - return mt; - } - var out = '' + text + '' : '>'; - return out; + + var warning = h('div.cp-inline-img-warning', [ + h('div.cp-inline-img', [ + h('img.cp-inline-img', { + src: '/images/broken.png', + //title: title || '', // FIXME sort out tippy issues (double-title) + }), + h('p.cp-alt-txt', text), + ]), + h('span.cp-img-block-notice', { + }, Messages.resources_imageBlocked), + h('br'), + h('a.cp-remote-img', { + href: qualifiedHref(href), + }, [ + Messages.resources_openInNewTab + ]), + h('br'), + h('a.cp-learn-more', { + href: Pages.localizeDocsLink('https://docs.cryptpad.fr/en/user_guide/security.html#remote-content'), + }, [ + Messages.resources_learnWhy + ]), + ]); + + return warning.outerHTML; }; restrictedRenderer.image = renderer.image; @@ -473,6 +537,75 @@ define([ } }; + var applyCSS = function (el, css) { + var style = h('style'); + style.appendChild(document.createTextNode(css)); + el.innerText = ''; + el.appendChild(style); + }; + + // trim non-functional text from less input so that + // the compiler is only triggered when there has been a functional change + var canonicalizeLess = function (source) { + return (source || '') + // leading and trailing spaces are irrelevant + .trim() + // line comments are easy to disregard + .replace(/\/\/[^\n]*/g, '') + // lines with nothing but spaces and tabs can be ignored + .replace(/^[ \t]*$/g, '') + // consecutive newlines make no difference + .replace(/\n+/g, ''); + }; + + var rendered_less = {}; + var getRenderedLess = (function () { + var timeouts = {}; + return function (src) { + if (!rendered_less[src]) { return; } + if (timeouts[src]) { + clearTimeout(timeouts[src]); + } + // avoid memory leaks by deleting cached content + // 15s after it was last accessed + timeouts[src] = setTimeout(function () { + delete rendered_less[src]; + delete timeouts[src]; + }, 15000); + return rendered_less[src]; + }; + }()); + + plugins.less = { + name: 'less', + attr: 'less-src', + render: function renderLess ($el, opt) { + var src = canonicalizeLess($el.text()); + if (!src) { return; } + var el = $el[0]; + var rendered = getRenderedLess(src); + if (rendered) { return void applyCSS(el, rendered); } + + var scope = opt.scope.attr('id') || 'cp-app-code-preview-content'; + var scoped_src = '#' + scope + ' { ' + src + '}'; + //console.error("RENDERING LESS"); + Less.render(scoped_src, {}, function (err, result) { + // the console is the only feedback for users to know that they did something wrong + // but less rendering isn't intended so much as a feature but a useful tool to avoid + // leaking styles from the preview into the rest of the DOM. This is an improvement. + if (err) { + // we assume the compiler is deterministic. Something that returns an error once + // will do it again, so avoid successive calls by caching a truthy + // but non-functional string to block them. + rendered_less[src] = ' '; + return void console.error(err); + } + var css = rendered_less[src] = result.css; + applyCSS(el, css); + }); + }, + }; + var getAvailableCachedElement = function ($content, cache, src) { var cached = cache[src]; if (!Array.isArray(cached)) { return; } @@ -546,7 +679,8 @@ define([ // 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')]; + var type = el.getAttribute('data-plugin'); + var plugin = plugins[type]; if (!plugin) { return; } var src = canonicalizeMermaidSource(el.childNodes[0].wholeText); el.setAttribute(plugin.attr, src); @@ -559,7 +693,8 @@ define([ var scrollTop = $parent.scrollTop(); // iterate over rendered mermaid charts $content.find('pre[data-plugin]:not([processed="true"])').each(function (index, el) { - var plugin = plugins[el.getAttribute('data-plugin')]; + var type = el.getAttribute('data-plugin'); + var plugin = plugins[type]; if (!plugin) { return; } // retrieve the attached source code which it was drawn @@ -666,7 +801,11 @@ define([ if (typeof(patch) === 'string') { throw new Error(patch); } else { - DD.apply($content[0], patch); + try { + DD.apply($content[0], patch); + } catch (err) { + console.error(err); + } var $mts = $content.find('media-tag'); $mts.each(function (i, el) { var $mt = $(el).contextmenu(function (e) { @@ -721,9 +860,35 @@ define([ if (target) { target.scrollIntoView(); } }); + // replace remote images with links to those images + $content.find('div.cp-inline-img-warning').each(function (index, el) { + if (!el) { return; } + var link = el.querySelector('a.cp-remote-img'); + if (!link) { return; } + link.onclick = function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + common.openURL(link.href); + }; + }); + + // transform style tags into pre tags with the same content + // to be handled by the less rendering plugin + $content.find('style').each(function (index, el) { + var parent = el.parentElement; + var pre = h('pre', { + 'data-plugin': 'less', + 'less-src': canonicalizeLess(el.innerText), + style: 'display: none', + }, el.innerText); + parent.replaceChild(pre, 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')]; + var type = el.getAttribute('data-plugin'); + var plugin = plugins[type]; + if (!plugin) { return; } var $el = $(el); $el.off('contextmenu').on('contextmenu', function (e) { @@ -742,13 +907,17 @@ define([ // you can assume that the index of your rendered charts matches that // of those in the markdown source. var src = plugin.source[index]; - el.setAttribute(plugin.attr, src); + if (src) { + 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 { - plugin.render($el); + plugin.render($el, { + scope: $content, + }); } catch (e) { console.error(e); } return; } diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index a145ab23b..e079ad2b9 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -2401,7 +2401,14 @@ define([ } if (!APP.loggedIn) { msg = APP.newSharedFolder ? Messages.fm_info_sharedFolder : Messages._getKey('fm_info_anonymous', [ApiConfig.inactiveTime || 90]); - return $(common.fixLinks($box.html(msg))); + var docsLink = 'https://docs.cryptpad.fr/en/user_guide/user_account.html#account-types'; + $box.html(msg).find('a[href="#docs"]').each(function () { + $(this).attr({ + href: Pages.localizeDocsLink(docsLink), + target: '_blank', + }); + }); + return $(common.fixLinks($box)); } if (!msg || APP.store['hide-info-' + path[0]] === '1') { $box.hide(); @@ -2564,14 +2571,7 @@ define([ var getNewPadTypes = function () { var arr = []; AppConfig.availablePadTypes.forEach(function (type) { - if (type === 'drive') { return; } - if (type === 'teams') { return; } - if (type === 'contacts') { return; } - if (type === 'todo') { return; } - if (type === 'file') { return; } - if (type === 'accounts') { return; } - if (type === 'calendar') { return; } - if (type === 'poll') { return; } // replaced by forms + if (AppConfig.hiddenTypes.indexOf(type) !== -1) { return; } if (!APP.loggedIn && AppConfig.registeredOnlyTypes && AppConfig.registeredOnlyTypes.indexOf(type) !== -1) { return; diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 966724fa1..b8550fcac 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -127,9 +127,8 @@ define([ if (e || !data) { return void displayDefault(); } if (typeof data !== "number") { return void displayDefault(); } if (Util.bytesToMegabytes(data) > 0.5) { return void displayDefault(); } - var $img = $('').appendTo($container); - $img.attr('src', src); - $img.attr('data-crypto-key', 'cryptpad:' + cryptKey); + var mt = UI.mediaTag(src, cryptKey); + var $img = $(mt).appendTo($container); MT.displayMediatagImage(common, $img, function (err, $image) { if (err) { return void console.error(err); } centerImage($img, $image); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 66e1967cd..9f779a0a8 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -274,9 +274,9 @@ define([ } var pads = data.pads || data; - s.rpc.pin(pads, function (e, hash) { + s.rpc.pin(pads, function (e) { if (e) { return void cb({error: e}); } - cb({hash: hash}); + cb({}); }); }; @@ -289,9 +289,9 @@ define([ if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); } var pads = data.pads || data; - s.rpc.unpin(pads, function (e, hash) { + s.rpc.unpin(pads, function (e) { if (e) { return void cb({error: e}); } - cb({hash: hash}); + cb({}); }); }; @@ -394,9 +394,9 @@ define([ if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } var list = getCanonicalChannelList(false); - store.rpc.reset(list, function (e, hash) { + store.rpc.reset(list, function (e) { if (e) { return void cb(e); } - cb(null, hash); + cb(null); }); }; diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 0ec224984..4184b8482 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -986,47 +986,60 @@ define([ var state = team.roster.getState() || {}; var members = state.members || {}; - // Get pending owners - var md = team.listmap.metadata || {}; - if (Array.isArray(md.pending_owners)) { - // Get the members associated to the pending_owners' edPublic and mark them as such - md.pending_owners.forEach(function (ed) { - var member; - Object.keys(members).some(function (curve) { - if (members[curve].edPublic === ed) { - member = members[curve]; - return true; - } - }); - if (!member && teamData.owner) { - var removeOwnership = function (chan) { - ctx.Store.setPadMetadata(null, { - channel: chan, - command: 'RM_PENDING_OWNERS', - value: [ed], - }, function () {}); - }; - removeOwnership(teamData.channel); - removeOwnership(Util.find(teamData, ['keys', 'roster', 'channel'])); - removeOwnership(Util.find(teamData, ['keys', 'chat', 'channel'])); + var md; + nThen(function (waitFor) { + // Get pending owners + ctx.Store.getPadMetadata(null, { + channel: teamData.channel + }, waitFor(function (obj) { + if (obj && obj.error) { + md = team.listmap.metadata || {}; return; } - member.pendingOwner = true; - }); - } + md = obj; + })); + }).nThen(function () { + ctx.pending_owners = md.pending_owners; + if (Array.isArray(md.pending_owners)) { + // Get the members associated to the pending_owners' edPublic and mark them as such + md.pending_owners.forEach(function (ed) { + var member; + Object.keys(members).some(function (curve) { + if (members[curve].edPublic === ed) { + member = members[curve]; + return true; + } + }); + if (!member && teamData.owner) { + var removeOwnership = function (chan) { + ctx.Store.setPadMetadata(null, { + channel: chan, + command: 'RM_PENDING_OWNERS', + value: [ed], + }, function () {}); + }; + removeOwnership(teamData.channel); + removeOwnership(Util.find(teamData, ['keys', 'roster', 'channel'])); + removeOwnership(Util.find(teamData, ['keys', 'chat', 'channel'])); + return; + } + member.pendingOwner = true; + }); + } - // Add online status (using messenger data) - if (ctx.store.messenger) { - var chatData = team.getChatData(); - var online = ctx.store.messenger.getOnlineList(chatData.channel) || []; - online.forEach(function (curve) { - if (members[curve]) { - members[curve].online = true; - } - }); - } + // Add online status (using messenger data) + if (ctx.store.messenger) { + var chatData = team.getChatData(); + var online = ctx.store.messenger.getOnlineList(chatData.channel) || []; + online.forEach(function (curve) { + if (members[curve]) { + members[curve].online = true; + } + }); + } - cb(members); + cb(members); + }); }; // Return folders with edit rights available to everybody (decrypted pad href) @@ -1144,8 +1157,7 @@ define([ if (!teamData) { return void cb ({error: 'ENOENT'}); } var team = ctx.teams[teamId]; if (!team) { return void cb ({error: 'ENOENT'}); } - var md = team.listmap.metadata || {}; - var isPendingOwner = (md.pending_owners || []).indexOf(user.edPublic) !== -1; + var isPendingOwner = user.pendingOwner; nThen(function (waitFor) { var cmd = isPendingOwner ? 'RM_PENDING_OWNERS' : 'RM_OWNERS'; @@ -1364,42 +1376,60 @@ define([ var describeUser = function (ctx, data, cId, cb) { var teamId = data.teamId; if (!teamId) { return void cb({error: 'EINVAL'}); } + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); var team = ctx.teams[teamId]; - if (!team) { return void cb ({error: 'ENOENT'}); } + if (!teamData || !team) { return void cb ({error: 'ENOENT'}); } if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } if (!data.curvePublic || !data.data) { return void cb({error: 'MISSING_DATA'}); } var state = team.roster.getState(); var user = state.members[data.curvePublic]; - // It it is an ownership revocation, we have to set it in pad metadata first - if (user.role === "OWNER" && data.data.role !== "OWNER") { - revokeOwnership(ctx, teamId, user, function (err) { - if (!err) { return void cb(); } - console.error(err); - return void cb({error: err}); - }); - return; - } + var md; + nThen(function (waitFor) { + // Get pending owners + ctx.Store.getPadMetadata(null, { + channel: teamData.channel + }, waitFor(function (obj) { + if (obj && obj.error) { + md = team.listmap.metadata || {}; + return; + } + md = obj; + })); + }).nThen(function () { + user.pendingOwner = Array.isArray(md.pending_owners) && + md.pending_owners.indexOf(user.edPublic) !== -1; - // Viewer to editor - if (user.role === "VIEWER" && data.data.role !== "VIEWER") { - changeEditRights(ctx, teamId, user, true, function (obj) { - return void cb(obj); - }); - } + // It it is an ownership revocation, we have to set it in pad metadata first + if (user.role === "OWNER" && data.data.role !== "OWNER") { + revokeOwnership(ctx, teamId, user, function (err) { + if (!err) { return void cb(); } + console.error(err); + return void cb({error: err}); + }); + return; + } - // Editor to viewer - if (user.role !== "VIEWER" && data.data.role === "VIEWER") { - changeEditRights(ctx, teamId, user, false, function (obj) { - return void cb(obj); - }); - } + // Viewer to editor + if (user.role === "VIEWER" && data.data.role !== "VIEWER") { + changeEditRights(ctx, teamId, user, true, function (obj) { + return void cb(obj); + }); + } - var obj = {}; - obj[data.curvePublic] = data.data; - team.roster.describe(obj, function (err) { - if (err) { return void cb({error: err}); } - cb(); + // Editor to viewer + if (user.role !== "VIEWER" && data.data.role === "VIEWER") { + changeEditRights(ctx, teamId, user, false, function (obj) { + return void cb(obj); + }); + } + + var obj = {}; + obj[data.curvePublic] = data.data; + team.roster.describe(obj, function (err) { + if (err) { return void cb({error: err}); } + cb(); + }); }); }; @@ -2010,9 +2040,16 @@ define([ if (['drive', 'teams', 'settings'].indexOf(app) !== -1) { safe = true; } Object.keys(teams).forEach(function (id) { if (!ctx.teams[id]) { return; } + var proxy = ctx.teams[id].proxy || {}; + var nPads = proxy.drive && Object.keys(proxy.drive.filesData || {}).length; + var nSf = proxy.drive && Object.keys(proxy.drive.sharedFolders || {}).length; t[id] = { owner: teams[id].owner, name: teams[id].metadata.name, + channel: teams[id].channel, + numberPads: nPads, + numberSf: nSf, + roster: Util.find(teams[id], ['keys', 'roster', 'channel']), edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']), avatar: Util.find(teams[id], ['metadata', 'avatar']), viewer: !Util.find(teams[id], ['keys', 'drive', 'edPrivate']), diff --git a/www/common/pinpad.js b/www/common/pinpad.js index 7e9cd4ee2..de93f066f 100644 --- a/www/common/pinpad.js +++ b/www/common/pinpad.js @@ -26,23 +26,19 @@ var factory = function (Util, Rpc) { exp.send = rpc.send; // you can ask the server to pin a particular channel for you - exp.pin = function (channels, cb) { + exp.pin = function (channels, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); if (!Array.isArray(channels)) { - setTimeout(function () { - cb('[TypeError] pin expects an array'); - }); - return; + return void cb('[TypeError] pin expects an array'); } rpc.send('PIN', channels, cb); }; // you can also ask to unpin a particular channel - exp.unpin = function (channels, cb) { + exp.unpin = function (channels, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); if (!Array.isArray(channels)) { - setTimeout(function () { - cb('[TypeError] pin expects an array'); - }); - return; + return void cb('[TypeError] pin expects an array'); } rpc.send('UNPIN', channels, cb); }; @@ -70,23 +66,12 @@ var factory = function (Util, Rpc) { }; // if local and remote hashes don't match, send a reset - exp.reset = function (channels, cb) { + exp.reset = function (channels, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); if (!Array.isArray(channels)) { - setTimeout(function () { - cb('[TypeError] pin expects an array'); - }); - return; + return void cb('[TypeError] pin expects an array'); } - rpc.send('RESET', channels, function (e, response) { - if (e) { - return void cb(e); - } - if (!response.length) { - console.log(response); - return void cb('INVALID_RESPONSE'); - } - cb(e, response[0]); - }); + rpc.send('RESET', channels, cb); }; // get the combined size of all channels (in bytes) for all the diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 796640f6d..0fd250f80 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -748,8 +748,8 @@ define([ var privateDat = cpNfInner.metadataMgr.getPrivateData(); var origin = privateDat.fileHost || privateDat.origin; var src = data.src = data.src.slice(0,1) === '/' ? origin + data.src : data.src; - mediaTagEmbedder($(''), data); + var mt = UI.mediaTag(src, data.key); + mediaTagEmbedder($(mt), data); }); }).appendTo(toolbar.$bottomL).hide(); }; diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 3fd4869d1..5d3738cbb 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -145,8 +145,7 @@ define([ var hexFileName = secret.channel; var origin = data.fileHost || data.origin; var src = origin + Hash.getBlobPathFromHex(hexFileName); - return '' + - ''; + return UI.mediaTag(src, key).outerHTML; } return; }; diff --git a/www/common/translations/messages.bn_BD.json b/www/common/translations/archive/messages.bn_BD.json similarity index 100% rename from www/common/translations/messages.bn_BD.json rename to www/common/translations/archive/messages.bn_BD.json diff --git a/www/common/translations/messages.te.json b/www/common/translations/archive/messages.te.json similarity index 100% rename from www/common/translations/messages.te.json rename to www/common/translations/archive/messages.te.json diff --git a/www/common/translations/messages.uk.json b/www/common/translations/archive/messages.uk.json similarity index 100% rename from www/common/translations/messages.uk.json rename to www/common/translations/archive/messages.uk.json diff --git a/www/common/translations/messages.ca.json b/www/common/translations/messages.ca.json index ad8254798..0132646a2 100644 --- a/www/common/translations/messages.ca.json +++ b/www/common/translations/messages.ca.json @@ -282,7 +282,7 @@ "fm_info_template": "Conté tots els documents desats com plantilles i que podeu reutilitzar quan vulgueu crear un nou document.", "fm_info_recent": "Aquests documents s'han modificat o obert darrerament, per vós o per alguna persona col·laboradora.", "fm_info_trash": "Buideu la paperera per alliberar espai al vostre CryptDrive.", - "fm_info_anonymous": "No heu iniciat la sessió, per tant, els vostres documents caducaran d'aquí a 3 mesos (
    saber-ne més). Es desen al vostre navegador, per tant, si netegeu el vostre historial podríeu perdre'ls.
    Registreu-vos o Inicieu la sessió per mantenir-los accessibles.
    ", + "fm_info_anonymous": "No heu iniciat la sessió, per tant, els vostres documents caducaran d'aquí a 3 mesos. Es desen al vostre navegador, per tant, si netegeu el vostre historial podríeu perdre'ls.
    Registreu-vos o Inicieu la sessió per mantenir-los accessibles.
    ", "fm_info_sharedFolder": "Aquesta és una carpeta compartida. No heu iniciat cap sessió, pel que només podeu accedir en mode només de lectura.
    Registreu-vos o Inicieu la sessió per poder importar-ho al vostre CryptDrive i modificar-ho.", "fm_info_owned": "Es documents que es mostren són de la vostra propietat. Això vol dir que podeu eliminar-los permanentment del servidor quan vulgueu. Si ho feu, la resta de persones no podran accedir-hi mai més.", "fm_error_cantPin": "Error intern del servidor. Si us plau, torneu a garregar la pàgina i torneu a provar-ho.", diff --git a/www/common/translations/messages.de.json b/www/common/translations/messages.de.json index e07cdf090..226e57c7c 100644 --- a/www/common/translations/messages.de.json +++ b/www/common/translations/messages.de.json @@ -279,7 +279,7 @@ "fm_info_template": "Hier sind alle Dokumente enthalten, die als Vorlage gespeichert wurden und die du wiederverwenden kannst, um ein neues Pad zu erstellen.", "fm_info_recent": "Diese Pads wurden kürzlich von dir oder von Personen, mit denen du zusammenarbeitest, geöffnet oder geändert.", "fm_info_trash": "Leere den Papierkorb, um mehr freien Platz in deinem CryptDrive zu erhalten.", - "fm_info_anonymous": "Du bist nicht eingeloggt, daher laufen deine Dokumente nach {0} Tagen aus. Sie könnten durch Löschen des Browserverlaufs verloren gehen.
    Registriere dich (keine persönlichen Informationen benötigt) oder logge dich ein, um sie dauerhaft in deinem Drive zu speichern. Mehr zu registrierten Accounts.", + "fm_info_anonymous": "Du bist nicht eingeloggt, daher laufen deine Dokumente nach {0} Tagen aus. Sie könnten durch Löschen des Browserverlaufs verloren gehen.
    Registriere dich (keine persönlichen Informationen benötigt) oder logge dich ein, um sie dauerhaft in deinem Drive zu speichern. Mehr zu registrierten Accounts.", "fm_info_sharedFolder": "Dieser Ordner ist geteilt. Da du aber nicht eingeloggt bist, hast du nur einen schreibgeschützen Zugriff.
    Registriere oder logge ich ein, damit du diesen Ordner in dein CryptDrive importieren und bearbeiten kannst.", "fm_info_owned": "Diese Pads sind deine eigenen. Das heißt, dass du sie jederzeit vom Server entfernen kannst. Wenn du das machst, sind sie auch für andere Nutzer nicht mehr zugänglich.", "fm_error_cantPin": "Interner Serverfehler. Bitte lade die Seite neu und versuche es erneut.", @@ -1354,5 +1354,19 @@ "form_open": "Öffnen", "form_viewButton": "Anzeigen", "form_poll_hint": ": Ja, : Nein, : Akzeptabel", - "fc_open_formro": "Öffnen (als Teilnehmer)" + "fc_open_formro": "Öffnen (als Teilnehmer)", + "resources_openInNewTab": "In neuem Tab öffnen", + "resources_imageBlocked": "CryptPad hat ein externes Bild blockiert", + "admin_purpose_noanswer": "Möchte ich nicht sagen", + "admin_purpose_business": "Für ein Unternehmen oder eine kommerzielle Organisation", + "admin_purpose_education": "Für eine Schule, Hochschule oder Universität", + "admin_purpose_org": "Für eine gemeinnützige Organisation oder eine Interessengruppe", + "admin_purpose_personal": "Für mich selbst, Familie oder Freunde", + "admin_purpose_experiment": "Zum Testen der Plattform oder zur Entwicklung neuer Funktionen", + "admin_instancePurposeTitle": "Zweck der Instanz", + "admin_instancePurposeHint": "Warum betreibst du diese Instanz? Deine Antwort wird mit den Entwicklern geteilt, sofern die Telemetrie aktiviert ist.", + "admin_purpose_public": "Zur Bereitstellung eines kostenlosen Dienstes für die Allgemeinheit", + "resources_learnWhy": "Mehr über die Gründe erfahren", + "team_leaveOwner": "Bitte entferne dich von der Rolle des Eigentümers, bevor du das Teams verlässt. Beachte, dass Teams mindestens einen Eigentümer haben müssen. Bitte füge daher zunächst einen weiteren Eigentümer hinzu, sofern du derzeit der alleinige Eigentümer bist.", + "form_exportCSV": "Als CSV exportieren" } diff --git a/www/common/translations/messages.el.json b/www/common/translations/messages.el.json index 67c2e30a8..7bbcaf861 100644 --- a/www/common/translations/messages.el.json +++ b/www/common/translations/messages.el.json @@ -224,7 +224,7 @@ "fm_info_template": "Περιέχει όλα τα pads που έχουν αποθηκευτεί ως πρότυπα και μπορείτε να ξαναχρησιμοποιήσετε όταν δημιουργείτε ένα νέο pad.", "fm_info_recent": "Λίστα των πρόσφατα τροποποιημένων ή ανοιγμένων pads.", "fm_info_trash": "Αδειάστε τον κάδο σας για να απελευθερώσετε χώρο στο CryptDrive σας.", - "fm_info_anonymous": "Δεν έχετε συνδεθεί, οπότε τα pads σας θα διαγραφούν μετά από 3 μήνες (μάθετε περισσότερα). Εγγραφείτε ή Συνδεθείτε για να τα κρατήσετε επ' αόριστον.", + "fm_info_anonymous": "Δεν έχετε συνδεθεί, οπότε τα pads σας θα διαγραφούν μετά από 3 μήνες. Εγγραφείτε ή Συνδεθείτε για να τα κρατήσετε επ' αόριστον.", "fm_error_cantPin": "Εσωτερικό σφάλμα διακομιστή. Παρακαλούμε επαναφορτώστε τη σελίδα και προσπαθήστε ξανά.", "fm_viewListButton": "Προβολή λίστας", "fm_viewGridButton": "Προβολή πλέγματος", diff --git a/www/common/translations/messages.es.json b/www/common/translations/messages.es.json index 59b0b16db..34b53ef47 100644 --- a/www/common/translations/messages.es.json +++ b/www/common/translations/messages.es.json @@ -216,7 +216,7 @@ "upload_success": "Tu archivo ({0}) ha sido subido con éxito y fue añadido a tu drive.", "pinLimitReachedAlertNoAccounts": "Has llegado a tu límite de espacio", "previewButtonTitle": "Mostrar/esconder la vista previa Markdown", - "fm_info_anonymous": "No has iniciado sesión, por lo que tus documentos expirarán después de {0} días. Borrar el historial de tu navegador puede hacer que desaparezcan.
    Inscríbete (no se requieren datos personales) o Ingresa para almacenarlos en tu disco de forma indefinida. Más información sobre las cuentas registradas.", + "fm_info_anonymous": "No has iniciado sesión, por lo que tus documentos expirarán después de {0} días. Borrar el historial de tu navegador puede hacer que desaparezcan.
    Inscríbete (no se requieren datos personales) o Ingresa para almacenarlos en tu disco de forma indefinida. Más información sobre las cuentas registradas.", "fm_error_cantPin": "Error del servidor. Por favor, recarga la página e inténtalo de nuevo.", "upload_notEnoughSpace": "No tienes suficiente espacio para este archivo en tu CryptDrive", "upload_tooLarge": "Este archivo supera el límite de carga permitido por tu cuenta.", diff --git a/www/common/translations/messages.fi.json b/www/common/translations/messages.fi.json index d1c5b9bb3..960853bc8 100644 --- a/www/common/translations/messages.fi.json +++ b/www/common/translations/messages.fi.json @@ -12,7 +12,8 @@ "todo": "Tehtävälista", "contacts": "Yhteystiedot", "sheet": "Taulukko", - "teams": "Teams" + "teams": "Teams", + "form": "Lomake" }, "button_newpad": "Uusi Teksti-padi", "button_newcode": "Uusi Koodi-padi", @@ -28,12 +29,12 @@ "padNotPinnedVariable": "Tämä padi vanhenee {4} päivän käyttämättömyyden jälkeen, {0}kirjaudu sisään{1} tai {2}rekisteröidy{3} säilyttääksesi sen.", "anonymousStoreDisabled": "Tämän CryptPad-instanssin ylläpitäjä on estänyt anonyymien käyttäjien pääsyn tallennustilaan. Kirjaudu sisään käyttääksesi CryptDrivea.", "expiredError": "Tämä padi on vanhentunut, eikä se ole enää saatavilla.", - "deletedError": "Tämä padi on poistettu omistajansa toimesta, eikä se ole enää saatavilla.", + "deletedError": "Tämä padi on poistettu, eikä se ole enää saatavilla.", "inactiveError": "Tämä padi on poistettu käyttämättömyyden vuoksi. Paina Esc-näppäintä luodaksesi uuden padin.", "chainpadError": "Sisältöä päivitettäessä tapahtui vakava virhe. Tämä sivu on vain luku-tilassa, jotta tekemäsi muutokset eivät katoaisi.
    Paina Esc-näppäintä jatkaaksesi padin katselua vain luku-tilassa, tai lataa sivu uudelleen yrittääksesi muokkaamista.", "invalidHashError": "Pyytämäsi asiakirjan URL-osoite on virheellinen.", "main_title": "CryptPad: Reaaliaikaista, kollaboratiivista editointia nollatietoperiaatteella", - "errorCopy": " Pääset yhä käyttämään asiakirjan sisältöä painamalla Esc-näppäintä.
    Suljettuasi tämän ikkunan sisältö katoaa, etkä voi enää käyttää sitä.", + "errorCopy": " Voit yhä käyttää nykyistä versiota vain luku-tilassa painamalla Esc-näppäintä.", "errorRedirectToHome": "PainaEsc-näppäintä ohjautuaksesi CryptDriveen.", "newVersionError": "Uusi versio CryptPadista on saatavilla.
    Lataa sivu uudelleen siirtyäksesi uuteen versioon, tai paina Esc-näppäintä käyttääksesi sisältöäsi offline-tilassa.", "loading": "Ladataan...", @@ -255,7 +256,7 @@ "fm_sharedFolderName": "Jaettu kansio", "fm_searchPlaceholder": "Hae...", "fm_newButton": "Uusi", - "fm_newButtonTitle": "Luo uusi padi tai kansio, tuo tiedosto nykyiseen kansioon", + "fm_newButtonTitle": "Luo uusi asiakirja tai kansio, tuo tiedosto nykyiseen kansioon.", "fm_newFolder": "Uusi kansio", "fm_newFile": "Uusi padi", "fm_morePads": "Lisää", @@ -286,7 +287,7 @@ "fm_info_template": "Sisältää kaikki mallipohjiksi tallennetut padit, joita voit käyttää uudelleen luodessasi uuden padin.", "fm_info_recent": "Tässä näytetään sinun tai yhteistyökumppaniesi äskettäin avaamat tai muokkaamat padit.", "fm_info_trash": "Tyhjennä roskakorisi vapauttaaksesi CryptDrive-tallennustilaa.", - "fm_info_anonymous": "Et ole kirjautunut sisään, joten luomasi asiakirjat vanhenevat {0} päivän päästä. Selaushistorian tyhjentäminen saattaa myös hävittää ne.
    Rekisteröidy (henkilötietoja ei tarvita) tai kirjaudu sisään säilyttääksesi luomasi asiakirjat pysyvästi CryptDrivessa. Lue lisää rekisteröitymisestä ja käyttäjätileistä.", + "fm_info_anonymous": "Et ole kirjautunut sisään, joten luomasi asiakirjat vanhenevat {0} päivän päästä. Selaushistorian tyhjentäminen saattaa myös hävittää ne.
    Rekisteröidy (henkilötietoja ei tarvita) tai kirjaudu sisään säilyttääksesi luomasi asiakirjat pysyvästi CryptDrivessa. Lue lisää rekisteröitymisestä ja käyttäjätileistä.", "fm_info_sharedFolder": "Tämä on jaettu kansio. Et ole kirjautunut sisään, joten voit käyttää sitä ainoastaan vain luku-tilassa.
    Rekisteröidy tai kirjaudu sisään tuodaksesi kansion omaan CryptDriveesi ja muokataksesi sen sisältöä.", "fm_info_owned": "Omistat tässä näytetyt padit. Se tarkoittaa, että voit halutessasi poistaa ne palvelimelta. Jos teet niin, muut käyttäjät eivät voi enää käyttää niitä.", "fm_error_cantPin": "Sisäinen palvelinvirhe. Ole hyvä ja lataa sivu uudelleen.", @@ -503,7 +504,7 @@ "mdToolbar_code": "Koodi", "mdToolbar_toc": "Sisällysluettelo", "home_host": "Tämä on itsenäinen yhteisön ylläpitämä Cryptpad-instanssi.", - "main_catch_phrase": "Avoimen lähdekoodin salattu kollaboraatioalusta", + "main_catch_phrase": "Kollaboraatioalusta
    päästä päähän -salattu ja avoin lähdekoodi", "footer_aboutUs": "Tietoa meistä", "about": "Tietoa meistä", "privacy": "Yksityisyys", @@ -963,9 +964,9 @@ "properties_addPassword": "Lisää salasana", "password_submit": "Lähetä", "password_placeholder": "Kirjoita salasana tähän...", - "password_error": "Padia ei löytynyt!
    Tämä virhe voi johtua kahdesta syystä: joko salasana on virheellinen tai padi on poistettu palvelimelta.", - "password_info": "Padia, jota yrität avata ei ole enää olemassa tai se on suojattu salasanalla. Syötä oikea salasana käyttääksesi padin sisältöä.", - "creation_newPadModalDescription": "Napsauta padityyppiä luodaksesi sellainen. Voit myös painaa Tab-näppäintä valitaksesi tyypin ja Enter vahvistaaksesi valinnan.", + "password_error": "Asiakirjaa ei löytynyt
    Tämä virhe voi johtua kahdesta syystä: joko salasana on virheellinen tai asiakirja on poistettu palvelimelta.", + "password_info": "Asiakirja, jota yrität avata ei ole enää olemassa tai se on suojattu uudella salasanalla. Syötä oikea salasana käyttääksesi asiakirjan sisältöä.", + "creation_newPadModalDescription": "Napsauta haluamaasi asiakirjatyyppiä luodaksesi sellaisen. Voit myös painaa Tab-näppäintä valitaksesi tyypin ja Enter-näppäintä vahvistaaksesi valinnan.", "creation_passwordValue": "Salasana", "creation_expiration": "Vanhenemispäivämäärä", "creation_noOwner": "Ei omistajaa", @@ -984,7 +985,7 @@ "creation_404": "Tätä padia ei ole enää olemassa. Käytä seuraavaa lomaketta uuden padin luontiin.", "feedback_optout": "Jos haluat jättäytyä pois tästä toiminnallisuudesta, voit tehdä sen Käyttäjäasetukset-sivulta löytyvän käyttäjäpalautevalintaruudun avulla.", "feedback_privacy": "Välitämme yksityisyydestäsi, ja haluamme samalla tehdä CryptPadista mahdollisimman helppokäyttöisen. Käytämme tätä tiedostoa selvittääksemme, mitkä käyttöliittymätoiminnot ovat tärkeitä käyttäjillemme, pyytämällä sitä tehdyn toiminnon kertovan parametrin yhteydessä.", - "feedback_about": "Jos luet tätä, haluat todennäköisesti tietää, miksi CryptPad lähettää pyyntöjä web-sivuille tiettyjen toimintojen yhteydessä", + "feedback_about": "Jos luet tätä, haluat todennäköisesti tietää, miksi CryptPad lähettää pyyntöjä web-sivuille tiettyjen toimintojen yhteydessä.", "settings_cat_kanban": "Kanban", "kanban_body": "Sisältö", "logoutEverywhere": "Kirjaudu ulos kaikkialta", @@ -1102,5 +1103,12 @@ "admin_archiveTitle": "Arkistoi asiakirjoja", "errorPopupBlocked": "CryptPadin täytyy pystyä avaamaan uusia välilehtiä toimiakseen. Ole hyvä ja salli ponnahdusikkunat selaimesi osoitekentästä. Ponnahdusikkunoita ei koskaan käytetä mainostamiseen.", "unableToDisplay": "Asiakirjan näyttäminen epäonnistui. Paina Esc-näppäintä ladataksesi sivun uudelleen. Jos ongelma ei ratkea, ota yhteyttä käyttäjätukeen.", - "documentID": "Asiakirjan tunniste" + "documentID": "Asiakirjan tunniste", + "whatis_collaboration_info": "

    CryptPad on rakennettu yhteistyötä varten. Se synkronoi asiakirjoihin tehdyt muutokset reaaliajassa. Kaikki data on salattua, eivätkä palvelu ja sen ylläpitäjät pääse tarkkailemaan asiakirjojen muokkausta tai niihin tallennettua sisältöä.

    ", + "register_warning_note": "CryptPadin salausrakenteesta johtuen palvelun ylläpitäjät eivät voi palauttaa tietojasi, jos unohdat käyttäjätunnuksesi ja/tai salasanasi. Säilytäthän ne turvallisessa paikassa.", + "register_notes": "", + "home_support": "

    Kehitystiimi ei hyödy käyttäjädatasta millään tavalla. Tämä kuuluu näkemykseemme yksityisyyttä kunnioittavista verkkopalveluista. Toisin kuin itseään \"ilmaisina\" mainostavat verkkoalustat, jotka monetisoivat käyttäjädataa, haluamme rakentaa kestävän käyttäjien omaan tahtoon perustuvan rahoitusmallin.

    Voit tukea projektia kertaluonteisella tai jatkuvalla lahjoituksella Open Collective -palvelussamme. Budjettimme on läpinäkyvä ja päivityksiä siihen julkaistaan säännöllisesti. Voit osallistua myös muilla kuin rahallisilla tavoilla.

    ", + "settings_padOpenLinkLabel": "Ota käyttöön suora linkkien avaus", + "settings_padOpenLinkHint": "Tämä asetus avaa upotetut linkit suoraan napsauttamalla ilman esikatseluikkunaa", + "settings_padOpenLinkTitle": "Avaa linkit ensimmäisellä napsautuksella" } diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json index 2193de407..1ea01009b 100644 --- a/www/common/translations/messages.fr.json +++ b/www/common/translations/messages.fr.json @@ -283,7 +283,7 @@ "fm_info_template": "Contient tous les fichiers que vous avez sauvés en tant que modèle afin de les réutiliser lors de la création d'un nouveau pad.", "fm_info_recent": "Ces pads ont été récemment ouverts ou modifiés par vous ou vos collaborateurs.", "fm_info_trash": "Vider la corbeille permet de libérer de l'espace dans votre CryptDrive.", - "fm_info_anonymous": "Vous n'êtes pas connecté, ces documents seront donc supprimés après {0} jours. Nettoyer l'historique de votre navigateur peut les faire disparaître.
    Enregistrez vous (aucune information personelle requise) ou Connectez vous pour les stocker de manière permanente dans votre drive. En lire plus sur les comptes utilisateurs.", + "fm_info_anonymous": "Vous n'êtes pas connecté, ces documents seront donc supprimés après {0} jours. Nettoyer l'historique de votre navigateur peut les faire disparaître.
    Enregistrez vous (aucune information personelle requise) ou Connectez vous pour les stocker de manière permanente dans votre drive. En lire plus sur les comptes utilisateurs.", "fm_info_sharedFolder": "Voici le contenu d'un dossier partagé. Il n'est accessible qu'en lecture seule car vous n'êtes pas connecté.
    Inscrivez-vous ou connectez-vous pour pouvoir l'importer dans votre CryptDrive et le modifier.", "fm_info_owned": "Vous êtes propriétaire des pads affichés dans cette catégorie. Cela signifie que vous pouvez choisir de les supprimer définitivement du serveur à n'importe quel moment. Ils seront alors inaccessibles pour tous les autres utilisateurs.", "fm_error_cantPin": "Erreur interne du serveur. Veuillez recharger la page et essayer de nouveau.", @@ -1354,5 +1354,19 @@ "button_newform": "Nouveau formulaire", "share_formView": "Participant", "share_formAuditor": "Auditeur", - "share_formEdit": "Auteur" + "share_formEdit": "Auteur", + "admin_purpose_noanswer": "Je préfère ne pas répondre", + "admin_purpose_experiment": "Pour tester la plateforme ou développer de nouvelles fonctionnalités", + "resources_imageBlocked": "CryptPad a bloqué une image distante", + "resources_openInNewTab": "Ouvrir dans un nouvel onglet", + "resources_learnWhy": "En savoir plus sur les images bloquées", + "admin_instancePurposeTitle": "Usage de l'instance", + "admin_purpose_personal": "Usage personnel, famille ou amis", + "admin_purpose_org": "Usage associatif", + "admin_purpose_education": "Usage éducatif, école ou université", + "admin_purpose_public": "Service gratuit ouvert au public", + "admin_purpose_business": "Usage en entreprise", + "admin_instancePurposeHint": "À quel usage cette instance est-elle destinée ? Votre réponse sera utilisée pour définir la planification de nouvelles fonctionnalités (si votre télémétrie est activée).", + "team_leaveOwner": "Veuillez vous rétrograder de votre rôle de propriétaire avant de quitter l'équipe. Notez que les équipes doivent avoir au moins un propriétaire, veuillez en ajouter un autre avant de poursuivre si vous êtes actuellement le seul propriétaire.", + "form_exportCSV": "Exporter en CSV" } diff --git a/www/common/translations/messages.it.json b/www/common/translations/messages.it.json index 2910e0ce2..5cade40ab 100644 --- a/www/common/translations/messages.it.json +++ b/www/common/translations/messages.it.json @@ -282,7 +282,7 @@ "fm_info_template": "Contiene tutti i pad salvati come modelli e che puoi riutilizzare per creare nuovi pad.", "fm_info_recent": "Questi pad sono stati recentemente aperti o modificati da te o da persone con le quali tu collabori.", "fm_info_trash": "Svuota il tuo cestino per liberare spazio nel tuo CryptDrive.", - "fm_info_anonymous": "Non hai effettuato l'accesso, quindi i tuoi pad scadranno fra tre mesi (scopri di più). Sono conservati nel tuo browser, quindi cancellando la cronologia potresti farli scomparire.
    Registrati o Accedi per conservarli permanentemente.
    ", + "fm_info_anonymous": "Non hai effettuato l'accesso, quindi i tuoi pad scadranno fra tre mesi. Sono conservati nel tuo browser, quindi cancellando la cronologia potresti farli scomparire.
    Registrati o Accedi per conservarli permanentemente.
    ", "fm_info_sharedFolder": "Questa è una cartella condivisa. Non hai effettuato l'accesso, quindi puoi visualizzarla solo in modalità di sola lettura.
    Registrati o Accedi per poterla importare nel tuo CryptDrive e per modificarla.", "fm_info_owned": "Sei il proprietario dei pad mostrati qui. Questo significa che puoi rimuoverli permanentemente dal server quando lo desideri. Se lo fai, gli altri utenti non potranno più accedervi.", "fm_error_cantPin": "Errore interno del server. Ricarica la pagina e prova di nuovo.", diff --git a/www/common/translations/messages.ja.json b/www/common/translations/messages.ja.json index 3751f0cbb..714cb4c00 100644 --- a/www/common/translations/messages.ja.json +++ b/www/common/translations/messages.ja.json @@ -1,7 +1,7 @@ { - "common_connectionLost": "サーバーとの接続が切断しました。\nサーバーと再接続するまで閲覧モードになります。", + "common_connectionLost": "サーバーとの接続が切断しました
    再接続するまで閲覧モードになります。", "button_newsheet": "新規スプレッドシート", - "button_newkanban": "新規看板", + "button_newkanban": "新規のカンバン", "button_newwhiteboard": "新規ホワイトボード", "button_newslide": "新規プレゼンテーション", "button_newpoll": "新規投票・アンケート", @@ -10,24 +10,25 @@ "type": { "teams": "チーム", "contacts": "連絡先", - "todo": "やることリスト", + "todo": "タスクリスト", "media": "メディア", "file": "ファイル", "whiteboard": "ホワイトボード", "drive": "CryptDrive", - "slide": "プレゼン", - "kanban": "看板", + "slide": "プレゼンテーション", + "kanban": "カンバン", "poll": "投票", "code": "コード", "pad": "リッチテキスト", - "sheet": "シート" + "sheet": "スプレッドシート", + "form": "フォーム" }, - "main_title": "CryptPad - 安全にリアルタイム編集可能なコラボレーションツール", + "main_title": "CryptPad - 安全にリアルタイムで編集可能なコラボレーションツール", "support_formButton": "送信", "support_formMessage": "メッセージを入力…", "support_formContentError": "エラー:内容が空です", "support_formTitleError": "エラー:件名が空です", - "support_formTitle": "チケットの件名", + "support_formTitle": "新しいチケット", "support_cat_new": "新しいチケット", "support_answer": "返信", "support_remove": "チケットを削除", @@ -59,11 +60,11 @@ "fm_folder": "フォルダ", "fm_folderName": "フォルダ名", "fm_fileName": "ファイル名", - "crowdfunding_button2": "CryptPad を助ける", + "crowdfunding_button2": "CryptPadを支援", "fm_padIsOwned": "あなたはこのパッドの所有者です", - "creation_expiration": "有効期限", - "owner_removeText": "所有者", - "creation_owners": "所有者", + "creation_expiration": "有効期限を設定", + "owner_removeText": "オーナー", + "creation_owners": "オーナー", "download_mt_button": "ダウンロード", "fc_rename": "名前を変更", "forgotten": "ごみ箱へ移動", @@ -73,7 +74,7 @@ "fm_creation": "作成日時", "fm_lastAccess": "最終アクセス日時", "fm_type": "種類", - "team_inviteLinkLoading": "あなたのリンクを生成中", + "team_inviteLinkLoading": "リンクを生成しています", "download_step1": "ダウンロード中", "loading": "読み込み中...", "fm_searchPlaceholder": "検索...", @@ -100,7 +101,7 @@ "settings_changePasswordNew": "新しいパスワード", "settings_changePasswordCurrent": "現在のパスワード", "settings_changePasswordButton": "パスワードを変更", - "settings_changePasswordHint": "アカウントのパスワードを変更します。「現在のパスワード」と、「新しいパスワード」および「新しいパスワードの確認」を入力してください。
    あなたがパスワードを忘れた場合、パスワードをリセットする方法はありません。細心の注意を払って、パスワードを安全に管理してください。", + "settings_changePasswordHint": "アカウントのパスワードを変更します。「現在のパスワード」と、「新しいパスワード」および「新しいパスワードの確認」を入力してください。
    パスワードを忘れた場合、再設定する方法はありません。細心の注意を払って、パスワードを安全に管理してください。", "settings_changePasswordTitle": "パスワードの変更", "languageButton": "言語", "language": "言語", @@ -119,28 +120,28 @@ "profile_addLink": "あなたのウェブサイトへのリンクを追加", "shareSuccess": "リンクをクリップボードにコピーしました", "shareButton": "共有", - "login_hashing": "パスワードをハッシュ化しています、この処理には時間がかかる場合があります。", + "login_hashing": "パスワードをハッシュ化しています。この処理には時間がかかる場合があります。", "login_invalPass": "パスワードを入力してください", "login_invalUser": "ユーザー名を入力してください", - "register_importRecent": "匿名セッション中のパッドをインポート", + "register_importRecent": "匿名セッションのドキュメントをインポート", "importButton": "インポート", "main_catch_phrase": "コラボレーションスイート
    暗号化されかつオープンソース", - "tos_3rdparties": "私たちは、法律で義務付けられている場合を除き、個別のデータを第三者に提供しません。", + "tos_3rdparties": "私たちは、法令に基づく場合を除き、個人情報を第三者に提供しません。", "tos_logs": "あなたのブラウザからサーバーに送信されたメタデータは、サービスを維持するために記録される場合があります。", "tos_availability": "私たちはこのサービスがあなたの役に立つことを願っていますが、可用性や性能は保証できません。定期的にデータをエクスポートしてください。", "tos_legal": "悪意ある行為、乱用する行為、または何らかの違法な行為を行わないでください。", - "tos_title": "CryptPad サービス利用規約", - "whatis_title": "CryptPad とは?", - "topbar_whatIsCryptpad": "CryptPad とは何か", + "tos_title": "CryptPadサービス利用規約", + "whatis_title": "CryptPadとは?", + "topbar_whatIsCryptpad": "CryptPadとは何か", "footer_tos": "利用規約", "footer_donate": "寄付", "footer_legal": "法的情報", "footer_aboutUs": "私たちについて", "pricing": "料金", "contact": "連絡先", - "privacy": "プライバシー", + "privacy": "プライバシーポリシー", "blog": "ブログ", - "register_header": "CryptPad へようこそ", + "register_header": "登録", "login_confirm": "パスワードの確認", "login_register": "新規登録", "login_username": "ユーザー名", @@ -150,7 +151,7 @@ "login_login": "ログイン", "autostore_hide": "保存しない", "autostore_store": "保存する", - "autostore_notstored": "この {0} は、あなたの CryptDrive に保存されていません。今すぐ保存しますか?", + "autostore_notstored": "この{0}はあなたのCryptDriveに保存されていません。今すぐ保存しますか?", "user_displayName": "表示名", "exportButton": "エクスポート", "user_rename": "表示名を変更", @@ -158,36 +159,36 @@ "saved": "保存しました", "error": "エラー", "deleted": "削除しました", - "profile_editDescription": "説明を編集", - "profile_addDescription": "説明を追加", + "profile_editDescription": "説明文を編集", + "profile_addDescription": "説明文を追加", "profileButton": "プロフィール", "profile_avatar": "アバター", "profile_upload": " 新しいアバターをアップロード", "teams_table_generic_edit": "編集: フォルダとパッドの作成、変更、削除が可能。", "teams_table_generic_view": "表示: フォルダとパッドへのアクセス(閲覧のみ)。", - "teams_table_generic_own": "チームの管理: チーム名とチームアバターの変更、所有者の追加または削除、チームのサブスクリプションの変更、チームの削除が可能。", + "teams_table_generic_own": "チームの管理: チーム名とチームのアバターの変更、オーナーの追加または削除、チームのサブスクリプションの変更、チームの削除が可能。", "teams_table_owners": "チームの管理", "teams_table_generic_admin": "メンバーの管理: メンバーの招待および取り消し、メンバーに管理者までの権限の付与が可能。", "teams_table_admins": "メンバーの管理", "teams_table_generic": "権限一覧", "teams_table": "権限", "contacts_fetchHistory": "古い履歴を取得する", - "contacts_warning": "ここに入力したすべてのものは永続的であり、このパッドの現在および将来のすべてのユーザーが利用できます。機密情報の入力は推奨されません!", - "contacts_typeHere": "メッセージを入力...", + "contacts_warning": "ここに入力した全てのメッセージは永続的であり、このパッドの現在および将来の全てのユーザーが確認できます。機密情報の入力には注意してください!", + "contacts_typeHere": "ここにメッセージを入力...", "team_cat_drive": "ドライブ", "team_cat_chat": "チャット", "team_cat_members": "メンバー", "team_cat_admin": "管理", "adminPage": "管理", "team_deleteButton": "削除", - "team_deleteHint": "チーム自体とチームが所有しているすべてのドキュメントを削除します。", + "team_deleteHint": "チームと、チームが所有している全てのドキュメントを削除します。", "team_deleteTitle": "チームの削除", "team_avatarHint": "容量 500KB 以下 (png 、jpg 、jpeg 、gif)", - "team_avatarTitle": "チームアバター", + "team_avatarTitle": "チームのアバター", "team_nameHint": "チームの名前を設定します", "team_nameTitle": "チーム名", "team_members": "メンバー", - "team_owner": "所有者", + "team_owner": "オーナー", "team_admins": "管理者", "viewers": "閲覧者", "contacts_padTitle": "チャット", @@ -220,7 +221,7 @@ "support_languagesPreamble": "サポートチームは次の言語に対応可能です:", "support_listHint": "管理者に送信されたチケットとその回答のリストは以下の通りです。閉じたチケットを再開することはできませんが、新しいチケットを作成することはできます。閉じたチケットは非表示にできます。", "support_listTitle": "サポートチケット", - "settings_padNotifCheckbox": "コメント通知を無効化", + "settings_padNotifCheckbox": "コメント通知を無効にする", "settings_padNotifTitle": "コメント通知", "notifications_dismissAll": "全て確認済みにする", "notifications_cat_archived": "履歴", @@ -228,57 +229,57 @@ "notifications_dismiss": "確認済みにする", "settings_autostoreMaybe": "手動 (確認しない)", "settings_autostoreNo": "手動 (常に確認する)", - "settings_autostoreHint": "自動 あなたがアクセスしたすべてのパッドを、あなたの CryptDrive に保存します。
    手動 (常に確認する) まだ保存していないパッドにアクセスした場合に、あなたの CryptDrive に保存するかどうか尋ねます。
    手動 (確認しない) アクセス先のパッドがあなたの CryptDrive に自動的に保存されなくなります。保存オプションは表示されなくなります。", + "settings_autostoreHint": "自動 アクセスした全てのパッドをCryptDriveに保存します。
    手動(常に確認) 保存していないパッドにアクセスした際、CryptDriveに保存するかどうかを確認します。
    手動(確認しない) アクセス先のパッドはCryptDriveに自動で保存されません。保存オプションは表示されません。", "settings_userFeedback": "ユーザーフィードバックを有効にする", "settings_userFeedbackHint2": "あなたのパッドのコンテンツがサーバーと共有されることはありません。", - "settings_userFeedbackHint1": "CryptPad は、あなたの経験を向上させる方法を知るために、サーバーにいくつかの非常に基本的なフィードバックを提供します。 ", + "settings_userFeedbackHint1": "CryptPadは、ユーザーエクスペリエンスの向上のため、いくつかの非常に基本的なフィードバックを、サーバーに提供します。 ", "settings_userFeedbackTitle": "フィードバック", "settings_autostoreYes": "自動", - "settings_importConfirm": "このブラウザで最近使用したパッドを、あなたのユーザーアカウントの CryptDrive にインポートしますか?", + "settings_importConfirm": "このブラウザで最近使用したパッドを、あなたのユーザーアカウントのCryptDriveにインポートしますか?", "settings_importDone": "インポートが完了しました", "settings_import": "インポート", - "settings_importTitle": "このブラウザでの最近のパッドをあなたの CryptDrive にインポートします", + "settings_importTitle": "このブラウザでの最近のパッドをあなたのCryptDriveにインポートします", "settings_trimHistoryHint": "ドライブと通知の履歴を削除して、ストレージ容量を節約します。これはパッドの履歴には影響しません。パッドの履歴は、プロパティダイアログから削除できます。", "trimHistory_currentSize": "現在の履歴容量: {0}", "support_cat_other": "その他", - "user_about": "CryptPad について", + "user_about": "CryptPadについて", "fc_delete": "ごみ箱へ移動", "fc_remove": "削除", "fc_restore": "復元", "fc_delete_owned": "完全削除", "creation_create": "作成", - "creation_password": "パスワードの追加", + "creation_password": "パスワード\n", "creation_expireMonths": "か月", "creation_expireDays": "日", "creation_expireHours": "時間", "creation_expireFalse": "無制限", "pad_wordCount": "単語数: {0}", "teams_table_role": "権限", - "templateSaved": "テンプレートを保存しました", + "templateSaved": "テンプレートを保存しました!", "saveTemplateButton": "テンプレートとして保存", "fm_rootName": "ドキュメント", "team_listTitle": "あなたのチーム", "team_createName": "チーム名", - "features_f_devices_note": "ユーザーアカウントでどこからでも CryptDrive にアクセスできます", + "features_f_devices_note": "ユーザーアカウントでどこからでもCryptDriveにアクセスできます", "features_f_devices": "全てのデバイスであなたのパッドを利用", "features_f_cryptdrive1_note": "フォルダ、共有フォルダ、テンプレート、タグ", - "features_f_cryptdrive1": "CryptDrive への完全なアクセス", - "features_f_anon_note": "匿名ユーザーが利用可能な全機能を利用できます", + "features_f_cryptdrive1": "CryptDriveの全機能", + "features_f_anon_note": "追加機能あり", "features_f_anon": "匿名ユーザーの全機能", - "features_f_storage0_note": "作成されたパッドは、3か月以上使用されないと削除される可能性があります", + "features_f_storage0_note": "ドキュメントは{0}日以上使用されないと削除されます", "features_f_storage0": "一時的な保存", - "features_f_cryptdrive0_note": "後で開けるようにブラウザにアクセスしたパッドを保存する機能", - "features_f_cryptdrive0": "CryptDrive への限定的なアクセス", - "features_f_file0_note": "他のユーザーが共有したファイルを表示およびダウンロードできます", - "features_f_file0": "ファイルを開く", + "features_f_cryptdrive0_note": "アクセスしたパッドをブラウザに保存して、後で開くことができます", + "features_f_cryptdrive0": "CryptDriveへの限定的なアクセス", + "features_f_file0_note": "他のユーザーが共有したドキュメントを表示およびダウンロードできます", + "features_f_file0": "ドキュメントを開く", "features_f_core_note": "編集、インポートとエクスポート、履歴、ユーザーリスト、チャット", - "features_f_core": "アプリケーションの一般的な機能", - "features_f_apps": "主なアプリケーションへのアクセス", - "features_premium": "プレミアムユーザー", - "features_registered": "登録ユーザー", - "features_title": "機能の比較", - "features_anon": "匿名ユーザー", - "register_whyRegister": "登録するとどの様な利点がありますか?", + "features_f_core": "一般的な機能", + "features_f_apps": "全アプリケーションへのアクセス", + "features_premium": "プレミアム", + "features_registered": "登録済", + "features_title": "機能", + "features_anon": "未登録", + "register_whyRegister": "登録するメリットをご紹介します", "historyText": "履歴", "help_button": "ヘルプ", "show_help_button": "ヘルプを表示", @@ -287,63 +288,63 @@ "okButton": "OK (enter)", "mustLogin": "このページにアクセスするにはログインする必要があります", "fm_noResult": "見つかりませんでした", - "fm_info_trash": "ごみ箱を空にすると、あなたの CryptDrive の使用可能容量を増やせます。", + "fm_info_trash": "ごみ箱を空にするとCryptDriveの使用可能容量を増やせます。", "features_f_file1": "ファイルのアップロードと共有", - "features_f_social_note": "プロフィールの作成、アバターの使用、連絡先とのチャット", - "features_f_social": "特別なアプリケーション", - "tos_e2ee": "CryptPad のコンテンツは、パッドのフラグメント識別子を推測または取得できる人物なら誰でも閲覧や編集が行えます。エンドツーエンド暗号化(E2EE)を採用したメッセンジャーサービスなどを使用してリンクを共有し、リンク漏洩が発生しないよう対策を行ってください。", + "features_f_social_note": "追加した連絡先との安全なコラボレーション、プロフィールの作成、きめ細かいアクセス権のコントロール", + "features_f_social": "ソーシャル機能", + "tos_e2ee": "CryptPadのコンテンツは、パッドのフラグメント識別子を推測または取得できる人物なら誰でも閲覧や編集が行えます。エンドツーエンド暗号化(E2EE)を採用したメッセンジャーサービスなどを使用してリンクを共有し、リンクの漏洩が発生しないよう対策を行ってください。リンクが漏洩した際に、責任を負うことはできません。", "contact_chat": "チャット", "contact_bug": "バグの報告", "footer_product": "製品", - "features_f_subscribe": "プレミアムプランに登録", + "features_f_subscribe": "定額利用を申し込む", "features_pricing": "月額 {0}€ ~ {2}€", "features_emailRequired": "メールアドレスが必要です", - "features_f_subscribe_note": "まず CryptPad にログインする必要があります", - "features_f_supporter": "プライバシーの支援者になる", - "features_f_supporter_note": "プライバシーを保護するソフトウェアが標準であることを世界に示すのを手伝うことができます", - "features_f_support_note": "チームプランには電子メールによる高度なサポートが付属します", + "features_f_subscribe_note": "定額利用の登録には、アカウントの登録が必要です", + "features_f_supporter": "プライバシーを支援", + "features_f_supporter_note": "CryptPadが財政的に持続可能になること、また、ユーザーが積極的に資金を提供するプライバシー強化ソフトウェアが標準でなければならないことを示す手助けをする", + "features_f_support_note": "電子メールとチケットシステムで、管理チームが優先対応", "features_f_support": "優先的なサポート", - "features_f_storage2_note": "選択したプランに応じて 5GB から 50GB までの追加ストレージ容量が利用可能です", + "features_f_storage2_note": "プランに応じて 5GB から 50GB までの追加ストレージ容量が利用でき、ファイルのアップロードの最大容量({0}MB)を増加", "features_f_storage2": "追加のストレージ容量", - "features_f_reg_note": "その上で CryptPad の開発者を支援できます", + "features_f_reg_note": "追加のメリット付き", "features_f_reg": "登録ユーザーの全機能", "homePage": "ホームページ", "features_noData": "登録に個人情報は必要ありません", "features_f_register": "無料登録", - "features_f_storage1_note": "CryptDrive に保存されたパッドが有効期限切れで削除されることはありません", - "features_f_storage1": "永続的ストレージ (50MB)", - "oo_sheetMigration_complete": "新しいバージョンが利用可能です。「OK」を押して再読み込みしてください。", - "oo_sheetMigration_loading": "あなたのスプレッドシートを最新バージョンにアップグレードしています", + "features_f_storage1_note": "CryptDriveに保存したドキュメントは、有効期限切れで削除されることはありません", + "features_f_storage1": "個人用ストレージ({0})", + "oo_sheetMigration_complete": "新しいバージョンが利用可能です。OKを押して再読み込みしてください。", + "oo_sheetMigration_loading": "あなたのスプレッドシートを最新バージョンにアップグレードしています。1分程度お待ちください。", "settings_ownDriveButton": "アカウントをアップグレード", - "features_f_file1_note": "連絡先とファイルを共有したり、パッドを埋め込む", - "crowdfunding_button": "CryptPad を支援", + "features_f_file1_note": "CryptDriveに画像ファイル、PDF、動画などを保存できます。保存したファイルは、連絡先と共有したり、ドキュメントに埋め込んだりできます。(最大容量は{0}MB)", + "crowdfunding_button": "CryptPadを支援", "contacts_removeHistoryTitle": "チャット履歴を削除", "properties_passwordSuccessFile": "パスワードは正常に変更されました。", "drive_sfPasswordError": "誤ったパスワードです", "team_title": "チーム: {0}", - "password_error": "パッドが存在しません!
    このエラーは「誤ったパスワードが入力された」場合、または「パッドがサーバーから削除された」場合に発生します。", + "password_error": "ドキュメントが存在しません!
    このエラーは、誤ったパスワードが入力された場合、またはドキュメントがサーバーから完全削除された場合に発生します。", "password_error_seed": "パッドが存在しません!
    このエラーは「パスワードが追加・変更された」場合、または「パッドがサーバーから削除された」場合に発生します。", "password_submit": "送信", "password_placeholder": "パスワードを入力...", - "password_info": "開こうとしているパッドが存在しないか、パスワードで保護されています。コンテンツにアクセスするには、正しいパスワードを入力してください。", + "password_info": "開こうとしているドキュメントが存在しないか、新しいパスワードで保護されています。コンテンツにアクセスするには、正しいパスワードを入力してください。", "properties_confirmNew": "パスワードを追加すると、このパッドの URL が変更され、履歴が削除されます。パスワードを知らないユーザーは、このパッドへアクセスできなくなります。続行してよろしいですか?", "properties_changePassword": "パスワードの変更", - "properties_addPassword": "パスワードの追加", + "properties_addPassword": "パスワードを設定", "history_close": "閉じる", "history_restore": "復元", - "fm_emptyTrashOwned": "ごみ箱に、あなたが所有しているドキュメントが入っています。あなたのドライブからのみ削除するか、すべてのユーザーから完全削除するかを選択できます。", + "fm_emptyTrashOwned": "ごみ箱に、あなたが所有しているドキュメントが入っています。あなたのドライブからのみ削除するか、全てのユーザーから完全削除するかを選択できます。", "access_destroyPad": "このドキュメントまたはフォルダを完全に削除する", "accessButton": "アクセス", "access_allow": "リスト", "makeACopy": "コピーを作成", "trimHistory_noHistory": "削除可能な履歴がありません", - "areYouSure": "クリックして実行", - "settings_safeLinksCheckbox": "安全なリンクを有効にする", - "settings_safeLinksTitle": "安全なリンク", - "settings_safeLinksHint": "CryptPad では、あなたのパッドを解読するための鍵がリンクに含まれています。これは、あなたのブラウザの閲覧履歴にアクセスできる人が、潜在的にあなたの CryptPad のデータを解読・閲覧できることを意味します。この「ブラウザの閲覧履歴にアクセスできる人」には、デバイス間で履歴を同期させる侵入的なブラウザ拡張機能やブラウザが含まれます。「安全なリンク」を有効にすると、鍵がブラウザの閲覧履歴に残ったり、アドレスバーに表示されたりするのを可能な限り防ぐことができます。この機能を有効にした上で共有メニューを使用することを強く推奨します。", - "settings_autostoreTitle": "CryptDrive へのパッドの保存", - "settings_logoutEverywhereConfirm": "すべてのデバイスでログインが取り消されるため、今後利用する際にもう一度ログインするよう求められます。続行しますか?", - "settings_logoutEverywhere": "他のすべてのウェブセッションからログアウトします", + "areYouSure": "よろしいですか?", + "settings_safeLinksCheckbox": "セーフリンクを有効にする", + "settings_safeLinksTitle": "セーフリンク", + "settings_safeLinksHint": "CryptPadでは、リンクの中にパッドを解読するための鍵が含まれています。ブラウザの閲覧履歴にアクセスできる人は、誰でもCryptPadのデータを閲覧することができます。ここにはデバイス間で履歴を同期するブラウザやその拡張機能も含まれます。「セーフリンク」を有効にすると、鍵がブラウザの閲覧履歴に残ったり、アドレスバーに表示されたりするのを可能な限り防ぐことができます。この機能を有効にして{0}の共有メニューを使用することを強く推奨します。", + "settings_autostoreTitle": "CryptDriveへのパッドの保存", + "settings_logoutEverywhereConfirm": "全てのデバイスで改めてログインしなければならなくなります。よろしいですか?", + "settings_logoutEverywhere": "他の全てのウェブセッションからログアウト", "settings_logoutEverywhereTitle": "リモートセッションを閉じる", "loading_state_5": "ドキュメントを再構築", "loading_state_4": "チームを読み込み", @@ -368,8 +369,8 @@ "snapshots_new": "新規スナップショット", "snaphot_title": "スナップショット", "snapshots_button": "スナップショット", - "filePicker_description": "埋め込むファイルを CryptDrive から選択するか、新規にアップロードしてください", - "uploadButtonTitle": "CryptDrive に新規ファイルをアップロード", + "filePicker_description": "埋め込むファイルをCryptDriveから選択するか、新規にアップロードしてください", + "uploadButtonTitle": "CryptDriveに新規ファイルをアップロード", "uploadFolderButton": "フォルダをアップロード", "uploadButton": "ファイルをアップロード", "filePicker_filter": "ファイル名で検索", @@ -385,11 +386,986 @@ "previewButtonTitle": "マークダウンのプレビューを表示または非表示にします", "whatis_model": "ビジネスモデル", "whatis_collaboration": "プライベートコラボレーション", - "home_support_title": "CryptPad を支援する", - "home_opensource": "CryptPad は、個人やプロなど誰でもホストすることができます。ソースコードは GitHub で確認できます。", + "home_support_title": "CryptPadを支援", + "home_opensource": "誰でもCryptPadを運営し、個人的または専門的な規模でサービスを提供することができます。ソースコードはGitHubで確認できます。", "home_opensource_title": "オープンソース", "home_host_title": "このインスタンスについて", - "home_privacy_text": "CryptPad は、データをプライベートに保護しながらコラボレーションを可能にするように構築されています。すべてのコンテンツは、あなたのブラウザ上で暗号化および復号されます。つまり、ドキュメント、チャット、およびファイルは、あなたがログインしているセッション以外では読み取れません。サービス管理者でさえ、あなたの情報にアクセスすることはできません。", - "home_privacy_title": "プライバシーバイデザイン", - "mdToolbar_tutorial": "https://www.markdowntutorial.com/jp/" + "home_privacy_text": "CryptPadは、データをプライベートに保護しながらコラボレーションを可能にするように構築されています。のコンテンツは、あなたのブラウザ上で暗号化および復号化されます。ドキュメント、チャット、およびファイルは、あなたがログインしているセッション以外では読み取れません。サービスの管理者でも、あなたの情報にアクセスすることはできません。", + "home_privacy_title": "プライバシー・バイ・デザイン", + "mdToolbar_tutorial": "https://www.markdowntutorial.com/jp/", + "languageButtonTitle": "シンタックスハイライトを行う言語を選択してください", + "useTemplate": "テンプレートで始めますか?", + "oo_uploaded": "アップロードが完了しました。OKをクリックしてページを再読み込みするか、キャンセルをクリックして閲覧モードを継続してください。", + "poll_descriptionHint": "投票の詳細を記入してください。入力が終了したら✓(公開)ボタンをクリックしてください。\n詳細はマークダウンの構文で記入できます。CryptDriveのメディアファイルを埋め込むこともできます。\nリンクを知っているひとは誰でも詳細を変更できますが、推奨されません。", + "pad_mediatagBorder": "枠線の幅(ピクセル)", + "openLinkInNewTab": "新しいタブでリンクを開く", + "history_restorePrompt": "ドキュメントの現在のバージョンを、表示しているバージョンに置き換えてよろしいですか?", + "slideOptionsTitle": "スライドをカスタマイズ", + "propertiesButtonTitle": "パッドのプロパティを表示", + "driveOfflineError": "CryptPadへの接続が切断されています。このパッドに加えられる変更はCryptDriveに保存されません。CryptPadののタブを閉じて、新しいウィンドウで開いてみてください。 ", + "properties_passwordWarningFile": "パスワードは変更されましたが、新しいデータでCryptDriveを更新することができませんでした。古いバージョンのファイルを手動で削除する必要があるかもしれません。", + "properties_confirmNewFile": "よろしいですか?パスワードを追加するとファイルのURLが変わります。パスワードをもたないユーザーは、このファイルにアクセスできなくなります。", + "properties_confirmChangeFile": "よろしいですか?新しいパスワードをもたないユーザーは、このファイルにアクセスできなくなります。", + "team_quota": "あなたのチームのストレージの容量制限", + "team_pendingOwner": "(保留中)", + "fm_contextMenuError": "そのアイテムのコンテクストメニューを開けません。問題が続くなら、ページを再読み込みしてみてください。", + "fm_selectError": "そのアイテムを選択できません。問題が続くなら、ページを再読み込みしてみてください。", + "fm_unknownFolderError": "選択、もしくは最後に開いたディレクトリは存在しません。親フォルダを開きます…", + "fm_restoreDialog": "{0}を元の場所に復元してよろしいですか?", + "fm_removeSeveralPermanentlyDialog": "ドライブから{0}個のアイテムを削除してよろしいですか?他のユーザーのドライブからは削除されません。", + "fm_removePermanentlyDialog": "ドライブからこのアイテムを削除してよろしいですか?他のユーザーのドライブからは削除されません。", + "fm_noname": "無題のドキュメント", + "fm_openParent": "フォルダに表示", + "fm_newButtonTitle": "新しいドキュメントやフォルダを作成したり、ファイルを現在のフォルダにインポートしたりできます。", + "contacts_info4": "チャットの参加者のどちらも履歴を削除できます", + "contacts_info2": "連絡先のアイコンをクリックしてチャットを開始", + "contacts_confirmRemove": "{0}をあなたの連絡先から削除してよろしいですか?", + "profile_error": "プロフィールの作成時にエラーが発生しました: {0}", + "profile_uploadSizeError": "エラー: アバターは{0}より小さい必要があります", + "kanban_addBoard": "ボードを追加", + "viewShare": "読み込み専用のリンク", + "mediatag_loadButton": "添付ファイルを読み込む", + "settings_disableThumbnailsAction": "CryptDriveでのサムネイルの作成を無効にする", + "settings_resetError": "入力した確認文が正しくありません。CryptDriveのデータは消去されていません。", + "settings_resetDone": "データが消去されました!", + "settings_resetPrompt": "あなたのドライブからのパッドを削除します。
    本当に続けてよろしいですか?
    続けるには「I love CryptPad」と入力してください。", + "settings_reset": "CryptDriveの全てのファイルとフォルダを削除", + "settings_exportErrorOther": "ドキュメントのエクスポート中にエラーが発生しました: {0}", + "drive_activeOld": "以前のパッド", + "settings_codeSpellcheckLabel": "コードエディターでスペルチェックを有効にする", + "cba_writtenBy": "著者: {0}", + "profile_copyKey": "公開鍵をコピー", + "oo_isLocked": "変更を同期しています。お待ちください", + "kanban_editBoard": "このボードを編集", + "kanban_editCard": "このカードを編集", + "kanban_clearFilter": "フィルターを消去", + "kanban_tags": "タグでフィルタリング", + "allow_text": "アクセスリストを使用すると、選択したユーザーとオーナーだけがドキュメントにアクセスできます。", + "admin_defaultlimitTitle": "ストレージの制限(MB)", + "owner_add": "{0}があなたをパッド「{1}」のオーナーになるよう希望しています。 承諾しますか?", + "owner_removeConfirm": "選択したユーザーのオーナー権を削除してよろしいですか? ユーザーには通知が送られます。", + "owner_removePendingText": "保留中", + "properties_unknownUser": "{0}人の不明なユーザー", + "requestEdit_viewPad": "パッドを新しいタブで開く", + "requestEdit_button": "編集権限を要求", + "support_notification": "管理人がサポートチケットに返答しました", + "support_showData": "ユーザーデータを表示/隠す", + "upload_mustLogin": "ファイルのアップロードにはログインが必要です", + "upload_choose": "ファイルを選択", + "form_page": "{0}/{1}ページ", + "form_addMultiple": "全て追加", + "admin_consentToContactLabel": "同意する", + "admin_updateAvailableButton": "リリースノートを見る", + "admin_updateAvailableHint": "CryptPadの新しいバージョンが利用可能です", + "admin_updateAvailableTitle": "新しいバージョン", + "userlist_addAsFriendTitle": "「{0}」に連絡先のリクエストを送信", + "contacts_remove": "この連絡先を削除", + "profile_register": "プロフィールの作成にはログインが必要です!", + "poll_removeOption": "このオプションを削除してよろしいですか?", + "share_linkTeam": "チームのドライブに追加", + "sharedFolders_create": "共有フォルダを作成", + "share_mediatagCopy": "Mediaタグをクリップボードにコピー", + "header_homeTitle": "CryptPadのホームページを開く", + "header_logoTitle": "CryptDriveを開く", + "settings_cursorShareTitle": "カーソルの位置を共有", + "settings_padWidthLabel": "エディターの幅を減らす", + "settings_codeIndentation": "コードエディターのインデント(空白スペース)", + "settings_export_done": "ダウンロードの準備ができました!", + "fc_openInCode": "コードエディターで開く", + "poll_create_option": "新しいオプションを追加", + "poll_create_user": "新しいユーザーを追加", + "pad_mediatagImport": "あなたのCryptDriveに保存", + "admin_listMyInstanceLabel": "このインスタンスをリストに表示する", + "admin_checkupTitle": "インスタンスの設定を検証", + "cba_disable": "消去して無効にする", + "upload_pending": "保留中", + "settings_resetTips": "ヒント", + "chrome68": "バージョン68のChromeもしくはChromiumを使用しているようです。このバージョンには、数秒経過した後でページが白紙になったり、クリックにページが反応しなくなったりするバグがあります。この問題を解決するには、別のタブを表示して改めて表示するか、このページでスクロールを試みてください。このバグは次のバージョンで解決される予定となっています。", + "register_notes": "", + "poll_commit": "送信", + "admin_removeDonateButtonTitle": "クラウドファンディングへの参加", + "mediatag_notReady": "ダウンロードを完了してください", + "pad_mediatagOpen": "ファイルを開く", + "pad_mediatagShare": "ファイルを共有", + "allowNotifications": "通知を許可", + "archivedFromServer": "ドキュメントがアーカイブされました", + "restoredFromServer": "ドキュメントが復元されました", + "admin_archiveInval": "無効なドキュメント", + "admin_archiveInput2": "ドキュメントのパスワード", + "admin_archiveInput": "ドキュメントのURL", + "admin_unarchiveTitle": "ドキュメントを復元", + "admin_archiveTitle": "ドキュメントをアーカイブ", + "fm_deletedFolder": "削除されたフォルダ", + "history_restoreDriveDone": "CryptDriveが復元されました", + "share_bar": "リンクを作成", + "settings_kanbanTagsTitle": "タグのフィルター", + "slide_textCol": "テキストの色", + "slide_backCol": "背景色", + "code_editorTheme": "エディターのテーマ", + "unknownPad": "不明なパッド", + "admin_openFilesTitle": "開かれているファイル", + "kanban_conflicts": "編集中:", + "kanban_noTags": "タグがありません", + "historyTrim_historySize": "履歴: {0}", + "team_links": "招待用リンク", + "team_cat_link": "招待用リンク", + "team_inviteTitle": "チームへの招待", + "team_inviteJoin": "チームに参加", + "team_inviteLinkCopy": "リンクをコピー", + "team_inviteLinkCreate": "リンクを作成", + "contacts_mutedUsers": "ミュートしたアカウント", + "sent": "メッセージが送信されました", + "team_inviteButton": "メンバーを招待", + "requestEdit_sent": "リクエストが送信されました", + "later": "あとで決める", + "admin_supportListTitle": "メールボックスのサポート", + "friendRequest_later": "あとで決める", + "admin_flushCacheButton": "キャッシュを消去", + "admin_registeredTitle": "登録ユーザー", + "crowdfunding_popup_no": "あとで", + "sharedFolders_create_name": "フォルダ名", + "creation_newTemplate": "新しいテンプレート", + "creation_noTemplate": "テンプレートがありません", + "creation_expire": "期限切れのパッド", + "mdToolbar_list": "箇条書き", + "uploadFolder_modal_filesPassword": "ファイルのパスワード", + "upload_title": "ファイルをアップロード", + "settings_cursorShowLabel": "カーソルを表示", + "settings_cursorColorTitle": "カーソルの色", + "settings_ownDriveTitle": "アカウントを更新", + "settings_driveDuplicateLabel": "重複したパッドを隠す", + "settings_export_compressing": "データを圧縮しています…", + "settings_cat_pad": "リッチテキスト", + "fc_collapseAll": "全て折りたたむ", + "fc_expandAll": "全て展開", + "fc_open_ro": "開く(読み取り専用)", + "fc_color": "色を変更", + "fm_passwordProtected": "パスワードで保護", + "poll_userPlaceholder": "あなたの名前", + "kanban_working": "作業中", + "kanban_newBoard": "新しいボード", + "pad_mediatagOptions": "画像のプロパティ", + "pad_mediatagRatio": "比率を維持", + "pad_mediatagHeight": "高さ(ピクセル)", + "pad_mediatagWidth": "幅(ピクセル)", + "pad_mediatagTitle": "Mediaタグの設定", + "admin_cat_network": "ネットワーク", + "form_clear": "消去", + "form_anonymous_on": "許可", + "form_open": "開く", + "form_input_ph_url": "https://example.com", + "form_input_ph_email": "email@example.com", + "form_backButton": "戻る", + "form_viewButton": "表示", + "form_form": "フォーム", + "form_editor": "エディタ", + "form_delete": "削除", + "form_sent": "送信しました", + "form_reset": "リセット", + "form_update": "更新", + "form_submit": "送信", + "form_type_md": "説明文", + "form_type_poll": "投票", + "form_type_checkbox": "チェックボックス", + "form_type_textarea": "段落", + "form_type_input": "文", + "form_text_number": "数", + "form_text_email": "Eメール", + "form_text_url": "リンク", + "settings_colortheme_light": "ライト", + "settings_colortheme_dark": "ダーク", + "settings_cat_style": "表示モード", + "admin_performanceKeyHeading": "コマンド", + "admin_performanceProfilingTitle": "性能", + "admin_cat_performance": "性能", + "redo": "やり直す", + "undo": "取り消す", + "settings_cacheTitle": "キャッシュ", + "admin_support_open": "表示", + "mediatag_saveButton": "保存", + "Offline": "オフライン", + "admin_unarchiveButton": "復元", + "admin_archiveButton": "アーカイブ", + "oo_version_latest": "最新", + "settings_cat_kanban": "カンバン", + "settings_kanbanTagsOr": "または", + "settings_kanbanTagsAnd": "かつ", + "pad_tocHide": "アウトライン", + "oo_refresh": "再読み込み", + "toolbar_file": "ファイル", + "drive_treeButton": "ファイル", + "toolbar_insert": "挿入", + "fm_sort": "並び替え", + "comments_comment": "コメント", + "comments_resolve": "解決", + "comments_reply": "返信", + "comments_submit": "送信", + "comments_edited": "編集済", + "cba_enable": "有効にする", + "canvas_select": "選択", + "kanban_body": "内容", + "kanban_title": "タイトル", + "teams": "チーム", + "teams_table_specific": "例外", + "properties_changePasswordButton": "送信", + "terms": "利用規約", + "poll_publish_button": "公開", + "team_viewers": "閲覧者", + "team_pending": "招待済", + "team_cat_create": "新規", + "team_inviteModalButton": "招待", + "owner_unknownUser": "不明", + "admin_cat_support": "サポート", + "supportPage": "サポート", + "share_withFriends": "共有", + "friendRequest_decline": "拒否", + "settings_codeSpellcheckTitle": "スペルチェック", + "contact_email": "Eメール", + "admin_cat_stats": "統計", + "markdown_toc": "コンテンツ", + "autostore_pad": "パッド", + "autostore_sf": "フォルダ", + "autostore_file": "ファイル", + "share_linkCopy": "コピー", + "creation_passwordValue": "パスワード", + "edit": "編集", + "features": "機能", + "mdToolbar_code": "コード", + "mdToolbar_quote": "引用", + "mdToolbar_link": "リンク", + "mdToolbar_strikethrough": "打ち消し線", + "mdToolbar_bold": "太字", + "mdToolbar_italic": "イタリック", + "mdToolbar_help": "ヘルプ", + "todo_title": "CryptTodo", + "download_step2": "復号化中", + "download_dl": "ダウンロード", + "upload_up": "アップロード", + "upload_cancelled": "キャンセル済", + "settings_padSpellcheckTitle": "スペルチェック", + "settings_resetThumbnailsAction": "消去", + "settings_thumbnails": "サムネイル", + "settings_resetTipsAction": "リセット", + "settings_resetButton": "削除", + "settings_cat_code": "コード", + "settings_cat_cursor": "カーソル", + "fc_hashtag": "タグ", + "fc_prop": "プロパティ", + "fc_remove_sharedfolder": "削除", + "contacts_send": "送信", + "contacts_title": "連絡先", + "poll_comment_list": "コメント", + "poll_optionPlaceholder": "オプション", + "kanban_done": "完了", + "kanban_delete": "削除", + "kanban_color": "色", + "calendar_import_temp": "このカレンダーをインポート", + "settings_deleteContinue": "アカウントを削除", + "admin_emailButton": "更新", + "button_newform": "新しいフォーム", + "form_editBlock": "編集", + "fm_deleteOwnedPads": "これらのパッドをサーバーから完全に削除してよろしいですか?", + "fm_deleteOwnedPad": "このパッドをサーバーから完全に削除してよろしいですか?", + "fm_sharedFolder": "共有フォルダ", + "fm_newFile": "新しいパッド", + "fm_newFolder": "新しいフォルダ", + "fm_newButton": "新規", + "fm_sharedFolderName": "共有フォルダ", + "fm_tagsName": "タグ", + "fm_filesDataName": "全てのファイル", + "contacts_leaveRoom": "このルームから退出", + "contacts_rooms": "ルーム", + "contacts_removeHistoryServerError": "チャットの履歴を削除する際にエラーが発生しました。後ほど再試行してください", + "contacts_confirmRemoveHistory": "チャットの履歴を削除してよろしいですか?データは復元できません", + "contacts_info3": "アイコンをダブルクリックしてプロフィールを表示", + "history_restoreDone": "ドキュメントが復元されました", + "history_restoreTitle": "選択したバージョンを復元", + "history_closeTitle": "履歴を閉じる", + "history_loadMore": "さらに履歴を読み込む", + "history_prev": "前のバージョン", + "history_next": "次のバージョン", + "historyButton": "ドキュメントの履歴を表示", + "notifyLeft": "{0}が共同セッションから退出しました", + "fileEmbedTag": "その後、ファイルを埋め込みたい任意のページの箇所に、このMedia Tagを配置してください:", + "themeButtonTitle": "コードとスライドのエディタのテーマ色を選択", + "printTransition": "遷移アニメーションを有効にする", + "admin_blockDailyCheckTitle": "サーバーのテレメトリー", + "admin_blockDailyCheckLabel": "サーバーのテレメトリーを無効にする", + "fc_open_formro": "開く(参加者として)", + "poll_unlocked": "編集可", + "poll_locked": "編集不可", + "poll_edit": "編集", + "poll_remove": "削除", + "poll_removeUser": "このユーザーを削除してよろしいですか?", + "oo_importInProgress": "インポート中です", + "register_emailWarning2": "他のサービスと異なり、メールアドレスを使ってパスワードをリセットすることはできません。", + "register_alreadyRegistered": "このユーザーは既に存在します。ログインしますか?", + "register_warning": "注意", + "register_cancel": "キャンセル", + "register_writtenPassword": "ユーザー名とパスワードをメモしました。続行", + "register_mustAcceptTerms": "利用規約に同意する必要があります。", + "register_passwordTooShort": "パスワードは最低{0}文字でなければなりません。", + "register_passwordsDontMatch": "パスワードが一致しません!", + "register_acceptTerms": "利用規約に同意", + "canvas_widthLabel": "幅: {0}", + "canvas_width": "幅", + "canvas_delete": "選択箇所を削除", + "canvas_clear": "消去", + "oo_cantUpload": "他のユーザーが在席中にアップロードすることはできません。", + "poll_comment_placeholder": "あなたのコメント", + "poll_comment_remove": "このコメントを削除", + "poll_comment_submit": "送信", + "poll_comment_add": "コメントを追加", + "imprint": "法定通知", + "oo_exportInProgress": "エクスポート中です", + "notifyJoined": "{0}が共同セッションに参加しました", + "viewEmbedTag": "パッドを埋め込むには、以下のiframeを任意の箇所に含めてください。CSSまたはHTMLの属性を使って装飾できます。", + "slideOptionsText": "オプション", + "tags_noentry": "削除したパッドにはタグ付けできません!", + "tags_duplicate": "重複タグ: {0}", + "tags_notShared": "タグは他のユーザーと共有されません", + "tags_add": "選択したパッドのタグを更新", + "tags_title": "タグ(あなた用)", + "filePickerButton": "CryptDriveに保存したファイルを埋め込む", + "printBackgroundRemove": "背景画像を削除", + "printBackgroundValue": "現在の背景: {0}", + "printBackgroundButton": "画像を選択", + "printBackground": "背景画像を使用", + "printTitle": "パッドのタイトルを表示", + "printDate": "日付を表示", + "printSlideNumber": "スライドの番号を表示", + "printOptions": "レイアウトのオプション", + "printButtonTitle2": "ドキュメントを印刷するかPDFファイルでエクスポート", + "printButton": "印刷(Enter)", + "printText": "印刷", + "backgroundButtonTitle": "プレゼンテーションの背景色を変更", + "template_empty": "利用できるテンプレートがありません", + "template_import": "テンプレートをインポート", + "useTemplateOK": "テンプレートを選択してください(Enter)", + "selectTemplate": "テンプレートを選択するかESCキーを押してください", + "saveTemplatePrompt": "テンプレートのタイトルを入力してください", + "newButtonTitle": "新しいパッドを作成", + "newButton": "新規", + "userAccountButton": "アカウント", + "userListButton": "ユーザーリスト", + "movedToTrash": "パッドをゴミ箱に移動しました。
    ドライブにアクセス", + "forgetPrompt": "OKをクリックするとパッドをゴミ箱へと移動します。よろしいですか?", + "forgetButton": "削除", + "pinLimitReached": "利用できるストレージの最大容量に達しました", + "disconnected": "接続が切れました", + "saveTitle": "タイトルを保存(Enter)", + "exportButtonTitle": "ローカルファイルにパッドをエクスポート", + "importButtonTitle": "ローカルファイルからパッドをインポート", + "pinLimitDrive": "ストレージの最大容量に達しました。
    新しいパッドは作成できません。", + "pinLimitNotPinned": "ストレージの最大容量に達しました。
    このパッドはCryptDriveに保存されません。", + "pinLimitReachedAlertNoAccounts": "ストレージの最大容量に達しました", + "pinLimitReachedAlert": "使用できるストレージの最大容量に達しました。新しいパッドはCryptDriveに保存されません。
    パッドをCryptDriveから削除するか、プレミアムユーザーになると容量を増やすことができます。", + "userlist_offline": "現在オフラインのため、ユーザーリストは利用できません。", + "readonly": "読み取り専用", + "errorState": "重大なエラー: {0}", + "realtime_unrecoverableError": "回復不能なエラーが発生しました。OKをクリックして再読み込みを行ってください。", + "disabledApp": "このアプリケーションは無効になっています。詳細については、このCryptPadの管理者にお問い合わせください。", + "deletedFromServer": "パッドは完全削除されました", + "newVersionError": "新しいバージョンのCryptPadがあります。
    リロードすると新しいバージョンを読み込みます。Escキーを押すとオフラインモードでコンテンツにアクセスします。", + "errorRedirectToHome": "Escキーを押すとCryptDriveにリダイレクトします。", + "errorCopy": " Escキーを押すと、閲覧モードで引き続きコンテンツにアクセスできます。", + "invalidHashError": "要求したドキュメントの URL が無効です。", + "chainpadError": "コンテンツを更新する際に重大なエラーが発生しました。コンテンツが失われないよう、閲覧モードで表示されています。
    このパッドを表示し続けるにはEscキーを押し、再度編集を試みるにはリロードをしてください。", + "inactiveError": "このパッドは使用されていなかったため削除されました。Escキーを押して新しいパッドを作成します。", + "deletedError": "このパッドは所有者によって削除されたため、使用できなくなりました。", + "expiredError": "このパッドは使用期限が過ぎてしまったため、使用できなくなりました。", + "anonymousStoreDisabled": "このCryptPadのインスタンスの管理者は、匿名ユーザーによる保存を無効に設定しています。CryptDriveを使用するにはログインする必要があります。", + "padNotPinnedVariable": "このパッドは{4}日使用しないと期限切れになります。{0}ログイン{1}または{2}登録{3}し保存してください。", + "padNotPinned": "このパッドは3ヶ月間使用しないと有効期限が切れます。{0}ログイン{1}するか{2}登録{3}して保存してください。", + "onLogout": "ログアウトしました。{0}ここをクリック{1}するか
    Escapeキーを押すと、閲覧モードでパッドにアクセスできます。", + "typeError": "このパッドは選択したアプリケーションと互換性がありません", + "form_type_page": "ページ分割", + "form_description_default": "ここにテキストを入力", + "team_pcsSelectHelp": "所有するパッドをチームのドライブに作成すると、そのパッドのオーナー権はチームに与えられます。", + "sharedFolders_create_owned": "フォルダを所有する", + "creation_owned1": "所有している項目は、オーナーの望むときにいつでも完全削除できます。完全削除すると、他のユーザーのCryptDriveからも削除されます。", + "creation_owned": "パッドを所有", + "uploadFolder_modal_owner": "所有するファイル", + "upload_modal_owner": "所有するファイル", + "settings_driveDuplicateHint": "所有するパッドを共有フォルダに移動すると、あなたのCryptDriveにパッドのコピーが保存され、あなたは引き続きそのパッドをコントロールできます。重複したファイルは隠すことができます。削除しない限り、共有したバージョンだけが表示されます。削除した場合は、以前の場所に元のファイルが表示されます。", + "settings_driveDuplicateTitle": "重複した所有するパッド", + "fm_info_owned": "あなたはここに表示されているパッドの所有者です。所有者は、サーバーからパッドを永久に削除することができます。削除すると、他のユーザーはパッドにアクセスできなくなります。", + "settings_deleteModal": "あなたのデータを削除するため、以下の情報をCryptPadの管理者と共有します。", + "oo_login": "ログインもしくは登録すると、スプレッドシートの性能が改善します。", + "cba_hint": "設定は次の新しいパッドから有効になります。", + "support_disabledHint": "このCryptPadのインスタンスはサポートフォームを利用するように設定されていません。", + "admin_supportInitTitle": "サポートメールボックスの初期化", + "admin_supportAddError": "秘密鍵が無効です", + "admin_supportAddKey": "秘密鍵を追加", + "todo_markAsIncompleteTitle": "このタスクを未完了にする", + "todo_markAsCompleteTitle": "このタスクを完了済にする", + "settings_codeFontSize": "コードエディターのフォントの大きさ", + "settings_anonymous": "ログインしていません。設定はこのブラウザのみで有効です。", + "settings_deleted": "アカウントが削除されました。OKを押すとホームページに移動します。", + "settings_deleteHint": "アカウントの削除は取り消せません。あなたのCryptDriveとパッドのリストはサーバーから削除されます。誰もCryptDriveに保存していないパッドは、90日で削除されます。", + "settings_resetThumbnailsDone": "サムネイルが消去されました。", + "settings_resetThumbnailsDescription": "ブラウザに保存したサムネイルを削除します。", + "settings_disableThumbnailsDescription": "新しいパッドを開くと、サムネイルが自動で作成され、ブラウザに保存されます。ここでサムネイルの作成を無効にできます。", + "fm_info_root": "フォルダを作成してファイルを整理できます。", + "oo_conversionSupport": "お使いのブラウザはMicrosoft Officeのフォーマットの変換に対応していません。FirefoxもしくはChromeの最新バージョンの使用を推奨します。", + "register_registrationIsClosed": "登録は締め切りました。", + "settings_notifCalendarHint": "今後のカレンダーのイベントの全ての通知を有効もしくは無効にします。", + "reminder_inProgressAllDay": "今日: {0}", + "reminder_inProgress": "{0}が{1}に開始しました", + "reminder_now": "{0}が開始しました", + "reminder_missed": "{0}が{1}で開催されました", + "calendar_dateRange": "{0} - {1}", + "calendar_import": "マイカレンダーに追加", + "calendar_errorNoCalendar": "編集可能なカレンダーが選択されていません", + "calendar_day": "日", + "calendar_new": "新しいカレンダー", + "calendar_default": "マイカレンダー", + "calendar": "カレンダー", + "pad_goToAnchor": "アンカーに移動", + "oo_cantMigrate": "この表はアップロードの最大のサイズを超えているため、移行することができません。", + "footer_roadmap": "ロードマップ", + "settings_deleteSubscription": "サブスクリプションを管理", + "broadcast_translations": "翻訳", + "admin_broadcastCancel": "メッセージを削除", + "admin_broadcastButton": "送信", + "broadcast_surveyURL": "アンケートのリンク", + "admin_surveyActive": "アンケートを開く", + "admin_surveyCancel": "削除", + "admin_surveyButton": "アンケートを保存", + "broadcast_newSurvey": "新しいアンケートがあります。クリックで開きます。", + "admin_surveyTitle": "アンケート", + "admin_maintenanceCancel": "メンテナンスをキャンセル", + "admin_maintenanceButton": "メンテナンスを予定", + "admin_maintenanceTitle": "メンテナンス", + "importError": "インポートできませんでした(誤ったフォーマット)", + "addOptionalPassword": "パスワードを追加(任意)", + "pad_settings_show": "表示", + "pad_settings_hide": "隠す", + "pad_settings_width_small": "ページモード", + "pad_settings_info": "このドキュメントの既定の設定です。新しいユーザーがドキュメントを閲覧したときに適用されます。", + "pad_settings_title": "ドキュメントの設定", + "admin_getquotaTitle": "アカウントのストレージを確認", + "settings_colorthemeTitle": "テーマ色", + "admin_getquotaButton": "確認", + "fm_cantUploadHere": "ここにはファイルをアップロードできません", + "settings_resetTipsDone": "全てのヒントが表示されます。", + "settings_resetTipsButton": "利用可能なCryptDriveのヒントをリセット", + "settings_resetNewTitle": "CryptDriveのデータを消去", + "settings_exportErrorMissing": "このドキュメントはサーバーにありません(期限切れ、もしくはオーナーにより削除されました)", + "settings_exportErrorEmpty": "このドキュメントはエクスポートできません(内容が空もしくは無効です)。", + "settings_exportErrorDescription": "以下のドキュメントをエクスポートに追加できませんでした:", + "settings_exportDescription": "ドキュメントをダウンロードして復号化しています。これには数分程度かかることがあります。タブを閉じると作業が中断されます。", + "crowdfunding_popup_text": "

    あなたの援助が必要です!

    CryptPadの開発が継続できるよう、OpenCollectiveのページからご支援いただきますようお願いします。ロードマップ資金調達の目標を同ページにて公開しています。", + "autostore_notAvailable": "この機能を使うにはCryptDriveにパッドを保存する必要があります。", + "autostore_forceSave": "CryptDriveにファイルを保存", + "autostore_saved": "パッドをCryptDriveに保存しました!", + "autostore_settings": "自動保存は設定ページで有効にできます!", + "broadcast_end": "終了", + "broadcast_start": "開始", + "broadcast_preview": "通知をプレビュー", + "team_inviteLinkError": "リンクの作成時にエラーが発生しました。", + "team_inviteLinkSetPassword": "リンクをパスワードで保護(推奨)", + "team_inviteLinkNote": "プライベート・メッセージを追加", + "contacts_muteInfo": "ミュートしたユーザーからは通知を受け取りません。
    ミュートしたことは相手に通知されません。 ", + "contacts_manageMuted": "ミュートを管理", + "notifyRenamed": "{0}は{1}になりました", + "printBackgroundNoValue": "背景画像が選択されていません", + "canvas_saveToDrive": "この画像をファイル形式でCryptDriveに保存", + "canvas_opacityLabel": "不透明度: {0}", + "canvas_opacity": "不透明度", + "poll_comment_disabled": "✓ボタンを押して投票を公開すると、コメントが有効になります。", + "kanban_item": "項目 {0}", + "fileEmbedScript": "このファイルを埋め込むには、Media Tagのロード用に、このスクリプトをページに一度含めてください。", + "editShare": "編集用リンク", + "colorButtonTitle": "プレゼンテーションモードの文字色を変更", + "presentButtonTitle": "プレゼンテーションモードに変更", + "exportPrompt": "ファイル名は何にしますか?", + "share_formEdit": "作者", + "history_userNext": "次の著者", + "history_userPrev": "以前の著者", + "comments_deleted": "コメントは著者により削除されました", + "cba_title": "著者の色", + "cba_hide": "著者の色を隠す", + "cba_show": "著者の色を表示", + "cba_properties": "著者の色(実験的)", + "restrictedError": "このドキュメントにアクセスする権限がありません", + "restrictedLoginPrompt": "このドキュメントにアクセスする権限がありません。アクセス権がある場合はログイン してください。", + "history_shareTitle": "このバージョンへのリンクを共有", + "admin_listMyInstanceTitle": "インスタンスを公開ディレクトリに表示", + "admin_checkupButton": "診断を行う", + "settings_driveRedirect": "自動で転送", + "mdToolbar_embed": "タイトルを埋め込む", + "copyToClipboard": "クリップボードにコピー", + "form_anonymousBox": "匿名で回答", + "form_anonymous_blocked": "匿名の回答はブロックされています。回答には ログインもしくは登録が必要です。", + "form_add_item": "項目を追加", + "form_type_radio": "選択肢", + "form_makePublicWarning": "回答を公開してよろしいですか?これは取り消せません。", + "form_notAnswered": "{0}個の未回答", + "form_showSummary": "概要を表示", + "form_showIndividual": "個々の回答を表示", + "form_results_empty": "回答がありません", + "form_results": "回答", + "form_answered": "このフォームは回答済みです", + "form_cantFindAnswers": "このフォームの既存の回答を取得できません。", + "form_duplicates": "重複する項目が削除されました", + "form_editType": "オプションの種類", + "share_formView": "参加者", + "admin_supportPrivHint": "他の管理者がサポートチケットを表示するのに必要な秘密鍵を表示します。この秘密鍵を入力するフォームは、管理パネルに表示されます。", + "oo_importBin": "OKをクリックするとCryptPadの内部フォーマット(.bin)をインポートします。", + "settings_notifCalendarTitle": "カレンダーの通知", + "reminder_minutes": "{0}はあと{1}分で開始します", + "upload_success": "ファイル({0})のアップロードが完了しました。", + "uploadFolder_modal_title": "フォルダーアップロードのオプション", + "upload_modal_title": "ファイルアップロードのオプション", + "settings_cursorShowTitle": "他のユーザーのカーソルの位置を表示", + "admin_authError": "このページには管理者のみアクセスできます", + "settings_cursorShareLabel": "位置を共有", + "settings_changePasswordNewPasswordSameAsOld": "新しいパスワードは現在のパスワードと異なるものでなければなりません。", + "login_unhandledError": "予期しないエラーが発生しました :(", + "autostore_error": "予期しないエラー: このパッドを保存できませんでした。再度試してください。", + "settings_changePasswordError": "予期しないエラーが発生しました。ログインしたりパスワードを変更したりすることができない場合は、CryptPadの管理者に連絡してください。", + "convertFolderToSF_confirm": "このフォルダを他の人が閲覧するには、共有フォルダに変更する必要があります。続行してよろしいですか?", + "share_linkPresent": "表示モード", + "share_linkEmbed": "埋め込みモード(ツールバーとユーザー一覧を隠します)", + "properties_passwordSuccess": "パスワードは変更されました。
    OKを押して再読み込みし、アクセス権限を更新してください。", + "properties_passwordWarning": "パスワードは変更されましたが、CryptDriveを更新することができませんでした。古いバージョンのパッドは手動で削除しなければならないかもしれません。
    OKを押して再読み込みし、アクセス権限を更新してください。", + "properties_confirmChange": "続行してよろしいですか?パスワードを変更すると履歴が削除されます。パスワードを知らないユーザーは、このパッドにアクセスできなくなります。", + "creation_newPadModalDescription": "作成するドキュメントの種類をクリックしてください。タブキーで選択し、エンターキーで作成することもできます。", + "creation_noOwner": "オーナーがいません", + "properties_passwordError": "パスワードの変更中にエラーが発生しました。再度試してください。", + "properties_passwordSame": "新しいパスワードは現在のパスワードと異なるものでなければなりません。", + "four04_pageNotFound": "お探しのページが見つかりませんでした。", + "about": "私たちについて", + "whatis_apps_info": "

    CryptPadは共同作業に必要なツールを備えた本格的なオフィススイートです。リッチテキスト、スプレッドシート、コード/マークダウン、カンバン、スライド、ホワイトボード、投票機能があります。

    それらのアプリケーションには、チャット、連絡先、著者別の色表示(コード/マークダウン)、コメント、メンション(リッチテキスト)などの機能が付属しています。

    ", + "mdToolbar_button": "マークダウンのツールバーを表示もしくは隠す", + "pad_base64": "このパッドはサイズの大きい画像を含んでいます。パッドのサイズが大きいと、読み込みに時間がかかります。サイズを減らすために、画像のフォーマットを変換して、別の画像ファイルとしてCryptDriveに保存することができます。画像を変換してよろしいですか?", + "todo_removeTaskTitle": "このタスクをリストから削除", + "upload_tooLargeBrief": "ファイルが{0}MBの制限を超えています", + "upload_tooLarge": "ファイルがあなたのアカウントでアップロードできるサイズを超えています。", + "upload_notEnoughSpaceBrief": "容量が足りません", + "upload_notEnoughSpace": "CryptDriveの空き容量が不足しています。", + "upload_uploadPending": "アップロード中です。キャンセルして新しいファイルをアップロードしますか?", + "upload_serverError": "サーバーエラー: ファイルをアップロードできませんでした。", + "uploadFolder_modal_forceSave": "ファイルをCryptDriveに保存", + "settings_export_download": "ドキュメントをダウンロードして復号化しています…", + "settings_export_reading": "CryptDriveを読み込んでいます…", + "settings_exportCancel": "エクスポートをキャンセルしてよろしいでしょうか?次のエクスポートでは最初からやりなおす必要があります。", + "fm_error_cantPin": "内部サーバーエラー。ページを再度読み込んでください。", + "fm_info_anonymous": "ログインしていないため、ドキュメントは{0}日後に期限切れになります。また、ブラウザの履歴を消去するとファイルが削除される恐れがあります。
    ファイルを永続的に保存するには、登録するか(個人情報の登録は不要です)ログインしてください。登録アカウントについては、こちらを参照してください。", + "fm_info_recent": "あなたか共同編集者が最近開いた、もしくは編集したパッドの一覧です。", + "fm_info_template": "テンプレートとして保存したパッドの一覧です。以下のテンプレートを使って、新しいパッドを作成することができます。", + "fm_categoryError": "選択したカテゴリーが開けません。ルートを表示します。", + "fm_morePads": "さらに表示", + "oo_reconnect": "サーバーの接続が回復しました。OKをクリックして再読み込みを行い、編集を継続してください。", + "changeNamePrompt": "名前を変更(匿名で利用する場合は空欄): ", + "form_text_text": "テキスト", + "form_poll_time": "時", + "form_poll_day": "日", + "form_poll_text": "テキスト", + "form_invalid": "無効なフォーム", + "admin_supportPrivButton": "鍵を表示", + "admin_supportInitGenerate": "サポートの鍵を生成", + "admin_supportPrivTitle": "サポートのメールボックスの秘密鍵", + "admin_emailHint": "あなたのインスタンスの連絡先のメールアドレスを設定してください", + "admin_emailTitle": "管理者の連絡先のメールアドレス", + "mediatag_defaultImageName": "画像", + "calendar_noNotification": "なし", + "genericCopySuccess": "クリップボードにコピーしました", + "toolbar_storeInDrive": "CryptDriveに保存", + "calendar_addNotification": "リマインダーを追加", + "calendar_notifications": "リマインダー", + "settings_notifCalendarCheckbox": "カレンダーの通知を有効にする", + "calendar_days": "日", + "calendar_hours": "時間", + "calendar_minutes": "分", + "calendar_allDay": "全日", + "calendar_location": "場所: {0}", + "calendar_loc": "場所", + "calendar_title": "タイトル", + "calendar_update": "更新", + "calendar_deleteOwned": "共有した他のユーザーには表示されたままになります。", + "calendar_deleteConfirm": "このカレンダーをあなたのアカウントから削除してよろしいですか?", + "calendar_today": "今日", + "calendar_month": "月", + "calendar_week": "週", + "calendar_deleteTeamConfirm": "このカレンダーをチームから削除してよろしいですか?", + "calendar_newEvent": "新しいイベント", + "settings_changePasswordPending": "パスワードを変更しています。完了するまでページを閉じたり、再読み込みしたりしないでください。", + "settings_changePasswordConfirm": "パスワードを変更してよろしいですか?全てのデバイスで再ログインする必要があります。", + "settings_ownDrivePending": "アカウントをアップグレードしています。完了するまでページを閉じたり、再読み込みしたりしないでください。", + "settings_ownDriveConfirm": "アカウントのアップグレードには時間がかかるかもしれません。全てのデバイスで再ログインする必要があります。よろしいですか?", + "settings_padOpenLinkLabel": "直接リンクを開く機能を有効にする", + "settings_padOpenLinkHint": "このオプションを有効にすると、プレビューの吹き出しを表示せず、埋め込んだリンクを直接クリックして開くことができます", + "settings_padOpenLinkTitle": "最初のクリックでリンクを開く", + "settings_padSpellcheckLabel": "リッチテキストのパッドでスペルチェックを有効にする", + "settings_padWidth": "エディターの最大幅", + "settings_codeUseTabs": "タブを使ってインデント(空白スペースの代わりに)", + "login_noSuchUser": "無効なユーザー名もしくはパスワードです。再度試すか、サインアップしてください", + "fo_unavailableName": "同じ名前のファイルまたはフォルダが既に存在しています。アイテムの名前を変更して、再度試してください。", + "fo_moveFolderToChildError": "フォルダをサブフォルダに移動することはできません", + "fo_existingNameError": "その名前はディレクトリで既に使用されています。他の名前を選んでください。", + "fo_moveUnsortedError": "フォルダをテンプレートのリストに移動することはできません", + "fm_moveNestedSF": "共有フォルダを他の共有フォルダの中に移動することはできません。フォルダ {0} は移動されませんでした。", + "fm_restoreDrive": "あなたのドライブを以前の状態に戻しています。最良の結果のため、完了するまでドライブに変更を加えるのは控えてください。", + "fm_padIsOwnedOther": "このパッドは他のユーザーにより所有されています", + "fm_burnThisDrive": "ブラウザに保存されているCryptPadの全ての情報を消去してよろしいですか?
    あなたのCryptDriveと履歴はブラウザから消去されますが、パッドは(暗号化されたまま)サーバー上に残ります。", + "fm_burnThisDriveButton": "ブラウザに保存されているCryptPadの全ての情報を消去", + "fm_canBeShared": "このフォルダは共有可能です", + "useTemplateCancel": "新しく開始(Esc)", + "fm_originalPath": "本来のパス", + "contacts_online": "このルームの別のユーザーがオンラインです", + "contacts_request": "{0}があなたを連絡先に追加しようとしています。承諾しますか?", + "contacts_rejected": "連絡先の招待が拒否されました", + "contacts_added": "連絡先の招待が承諾されました。", + "profile_uploadTypeError": "エラー: あなたのアバターの種類は許可されていません。許可されている種類: {0}", + "canvas_imageEmbed": "コンピューターの画像を埋め込む", + "canvas_currentBrush": "現在のブラシ", + "poll_total": "合計", + "kanban_todo": "未着手", + "slide_invalidLess": "ユーザー定義のスタイルは無効です", + "printCSS": "ユーザー定義のスタイルルール(CSS):", + "admin_provideAggregateStatisticsHint": "追加の利用状況を開発者に提供することも選択できます。追加の情報には、あなたのインスタンスのおおよその登録ユーザー数や一日あたりのユーザー数などがあります。", + "fm_info_sharedFolder": "これは共有フォルダです。ログインしていないため、閲覧モードでしかアクセスできません。
    登録もしくはログインすると、CryptDriveにインポートして編集できます。", + "team_invitedToTeam": "{0}があなたをチーム「{1}」に招待しました", + "team_inviteFromMsg": "{0}があなたをチーム「{1}」に招待しました", + "team_invitePleaseLogin": "招待を承諾するには、ログインもしくは登録してください。", + "team_inviteEnterPassword": "招待のパスワードを入力してください。", + "team_inviteGetData": "チームのデータを取得中です", + "team_inviteInvalidLinkError": "招待リンクが無効です。", + "admin_getlimitsHint": "インスタンスに適用されている全てのカスタムストレージの制限を一覧表示します。", + "settings_exportWarning": "注意:このツールはベータ版のため、スケーラビリティ上の問題があるかもしれません。よりよいパフォーマンスのために、このタブをフォーカスしたままにすることを推奨します。", + "settings_exportFailed": "1分以上ダウンロードにかかるパッドは、エクスポートしたファイルには含まれません。エクスポートされなかったパッドについては、リンクを表示します。", + "settings_exportTitle": "CryptDriveをエクスポート", + "settings_backup2Confirm": "あなたのCryptDriveの全てのパッドをダウンロードします。続けたい場合は、名前を入力してOKを押してください", + "settings_backup2": "CryptDriveをダウンロード", + "settings_backupHint2": "ドライブの全てのドキュメントをダウンロードします。ドキュメントは、他のアプリケーションで読み込めるフォーマットがあれば、そのフォーマットでダウンロードされます。そうしたフォーマットがなければ、CryptPadで読み込めるフォーマットでダウンロードされます。", + "settings_backupHint": "CryptDriveのコンテンツをバックアップもしくは復元します。バックアップには、パッドの内容ではなく、それにアクセスする鍵だけが含まれます。", + "register_emailWarning3": "その点を踏まえてなおユーザー名にメールアドレスを使用する場合は、OKをクリックしてください。", + "register_emailWarning1": "それもできますが、サーバーには送信されません。", + "register_emailWarning0": "ユーザー名にメールアドレスが入力されています。", + "fm_tags_used": "使用数", + "fm_deletedPads": "これらのパッドはサーバーにありません。あなたのCryptDriveから削除されています: {0}", + "fm_renamedPad": "このパッドにユーザー定義の名前を設定しました。共有のファイル名は「{0}」です", + "fm_ownedPadsName": "所有", + "burnAfterReading_generateLink": "下のボタンをクリックするとリンクを生成します。", + "fm_restricted": "アクセス権がありません", + "admin_performancePercentHeading": "割合", + "settings_cacheButton": "既存のキャッシュを消去", + "settings_cacheCheckbox": "このデバイスでキャッシュを有効にする", + "form_submitWarning": "強制的に送信", + "form_updateWarning": "強制的にアップデート", + "form_answerName": "{0}が{1}に回答", + "form_addMultipleHint": "複数の日時を追加", + "admin_consentToContactTitle": "連絡に同意", + "form_poll_hint": ": はい、: いいえ、: 可", + "admin_provideAggregateStatisticsLabel": "集計済みの統計を提供", + "admin_provideAggregateStatisticsTitle": "統計による集計", + "admin_blockDailyCheckHint": "CryptPadのインスタンスは、1日に1度、開発者のサーバーにメッセージを送信するよう設定されています。開発者はそれをもとに、CryptPadのバージョンごとのサーバー数を追跡しています。以下からこの測定をオプトアプトできます。なお、送信されるメッセージの内容は、確認用として、アプリケーションのサーバーのログに記録されます。", + "admin_listMyInstanceHint": "あなたのインスタンスが誰でも利用できる場合、同意によってウェブディレクトリに表示できます。表示には、サーバーのテレメトリーが有効になっている必要があります。", + "settings_driveRedirectTitle": "ホームページにリダイレクト", + "form_defaultItem": "項目{0}", + "form_defaultOption": "オプション{0}", + "form_answerAnonymous": "{0}に匿名の回答", + "form_maxLength": "文字数制限: {0}/{1}", + "form_textType": "テキストの種類", + "form_pollYourAnswers": "あなたの回答", + "form_pollTotal": "合計", + "form_editMaxLength": "最大文字数", + "form_add_option": "オプションを追加", + "form_newItem": "新しい項目", + "form_newOption": "新しいオプション", + "form_anonymous": "匿名の回答", + "form_removeEnd": "締切日を削除", + "form_setEnd": "締切日を設定", + "form_isPrivate": "回答は公開されていません", + "form_isPublic": "回答は公開されています", + "form_makePublic": "回答を公開", + "form_invalidQuestion": "質問{0}", + "info_privacyFlavour": "プライバシーポリシーに、データの取り扱い方を記載しています。", + "toolbar_tools": "ツール", + "toolbar_savetodrive": "画像として保存", + "comments_error": "ここにはコメントを追加できません", + "settings_padNotifHint": "あなたのコメントへの返信の通知を無視する", + "mentions_notification": "{0}があなたを{1}でメンションしました", + "access_muteRequests": "このパッドへのアクセスリクエストをミュート", + "access_noContact": "追加する連絡先がありません", + "trimHistory_needMigration": "この機能を有効にするにはCryptDriveをアップデートしてください。", + "trimHistory_error": "履歴を削除している途中でエラーが発生しました", + "trimHistory_getSizeError": "ドライブの履歴のサイズを計算している途中でエラーが発生しました", + "profile_login": "このユーザーを連絡先に追加するにはログインする必要があります", + "dontShowAgain": "再び表示しない", + "oo_sheetMigration_anonymousEditor": "登録ユーザーが最新のバージョンに更新するまで、このスプレッドシートを未登録ユーザーが編集することはできません。", + "oo_invalidFormat": "このファイルはインポートできません", + "burnAfterReading_warningDeleted": "このパッドは削除されました。ウインドウを閉じた後で再びアクセスすることはできません。", + "burnAfterReading_proceed": "表示して削除", + "team_leaveConfirm": "チームから退出すると、チームのCryptDrive、チャットの履歴などにアクセスできなくなります。よろしいですか?", + "team_leaveButton": "チームから退出", + "team_rosterKick": "チームからキック", + "team_rosterDemote": "降格", + "team_rosterPromote": "昇格", + "team_createLabel": "新しいチームを作成", + "team_kickedFromTeam": "{0}があなたをチーム「{1}」からキックしました", + "admin_diskUsageButton": "レポートを生成", + "admin_diskUsageTitle": "ディスクの使用状況", + "contact_adminHint": "アカウントに関連した問題、ストレージの制限、サービスの運用状況に関して。\n", + "contact_devHint": "機能のリクエスト、ユーザビリティの改善、もしくはお礼については、以下よりお願いします。", + "contact_dev": "開発者に連絡", + "contact_admin": "管理者に連絡", + "admin_flushCacheDone": "キャッシュを消去しました", + "admin_flushCacheTitle": "HTTPキャッシュを消去", + "admin_updateLimitDone": "アップデートが完了しました", + "admin_updateLimitButton": "クォータを更新", + "admin_registeredHint": "あなたのインスタンスの登録ユーザー数", + "admin_updateLimitTitle": "ユーザーのクォータを更新", + "mdToolbar_defaultText": "ここにテキストを入力", + "upload_modal_filename": "ファイル名(拡張子 {0} を自動で追加)", + "settings_padSpellcheckHint": "リッチテキストパッドでスペルチェックを有効にします。間違ったつづりには赤色の下線が表示されます。右クリックをしながらコントロールキーもしくはメタキーを押すと、正しい選択肢が表示されます。", + "creation_404": "このパッドは存在しません。以下のフォームより新しいパッドを作成してください。", + "help_genericMore": "CryptPadの使い方についてはドキュメンテーションをご覧ください。", + "whatis_drive": "CryptDriveでまとめて管理", + "mdToolbar_toc": "目次", + "mdToolbar_check": "タスクリスト", + "mdToolbar_nlist": "番号付きリスト", + "mdToolbar_heading": "見出し", + "settings_cursorShowHint": "共同ドキュメントであなたが他のユーザーのカーソルの位置を見られるかどうかを決められます。", + "settings_cursorShareHint": "共同ドキュメントで他のユーザーがあなたのカーソルの位置を見られるかどうかを決められます。", + "settings_cursorColorHint": "共同ドキュメントでの他のユーザーのカーソルの色を変更できます。", + "settings_padWidthHint": "テキストエディターの幅を制限するページモード(既定)と、スクリーン全体の幅を使用するモードを切り替えられます。", + "fm_forbidden": "禁止されたアクション", + "team_exportHint": "チームのドライブの全てのドキュメントをダウンロードします。ドキュメントは、他のアプリケーションで読み込めるフォーマットがあれば、そのフォーマットでダウンロードされます。そうしたフォーマットがなければ、CryptPadで読み込めるフォーマットでダウンロードされます。", + "admin_limitUser": "ユーザーの公開鍵", + "fm_shareFolderPassword": "このフォルダをパスワードで保護(任意)", + "team_exportTitle": "チームのドライブをダウンロード", + "admin_invalLimit": "無効な制限値です", + "admin_limitSetNote": "メモ", + "admin_limitMB": "制限(MB)", + "broadcast_newCustom": "管理者からのメッセージ", + "admin_broadcastActive": "アクティブなメッセージ", + "fc_noAction": "利用可能なアクションはありません", + "support_closed": "このチケットは終了しました", + "support_cat_tickets": "既存のチケット", + "support_formHint": "このフォームから、管理者に問題の報告や質問を安全に行うことができます。
    それらに関しては CryptPadユーザーガイドで回答されているものがあるかもしれません。既にチケットを作成している場合、同じ問題に関して新しいチケットを作成するのはお控えください。その代わりに、既存のチケットに追加の情報を送信してください。", + "support_disabledTitle": "サポートは有効になっていません", + "admin_supportListHint": "ユーザーからサポートのメールボックスに送信されたチケットの一覧です。管理者はメッセージと回答を閲覧できます。終了したチケットは再開することができます。終了したチケットについては削除する(隠す)ことができますが、他の管理者には引き続き表示されます。", + "drive_sfPassword": "共有フォルダ「 {0} 」は利用できません。オーナーにより削除されたか、新しいパスワードで保護されています。CryptDriveから削除するか、新しいパスワードを使ってアクセスを回復できます。", + "fm_info_sharedFolderHistory": "これは共有フォルダの履歴です: {0}
    CryptDriveは閲覧モードを継続します。", + "notification_folderShared": "{0}があなたとフォルダを共有しました: {1}", + "convertFolderToSF_SFChildren": "このフォルダは共有フォルダを含んでいるため、共有フォルダに変更できません。続けるには、このフォルダを共有フォルダの外に移動してください。", + "sharedFolders_share": "このリンクを登録ユーザーと共有すると、共有フォルダへのアクセスが可能になります。相手がリンクを開くと、CryptDriveに共有フォルダが追加されます。", + "sharedFolders_duplicate": "移動しようとしているパッドのいくつかは既に移動先のフォルダに存在しています。", + "sharedFolders_forget": "このパッドは共有フォルダにのみ保存されているため、ゴミ箱に移動できません。あなたのCryptDriveから削除することは可能です。", + "settings_ownDriveHint": "技術上の理由で、旧アカウントは最新の機能にアクセスできません。フリーのアップデートで新しい機能が有効になり、CryptDriveの今後のアップデートにも対応します。", + "admin_activePadsHint": "閲覧もしくは編集中のドキュメント数", + "admin_activePadsTitle": "アクティブなパッド", + "admin_activeSessionsTitle": "アクティブな接続", + "fm_expirablePad": "期日: {0}", + "survey": "CryptPadのアンケート", + "share_embedCategory": "埋め込む", + "feedback_optout": "オプトアウトを希望の場合は、設定画面のチェックボックスでフィードバックを無効に設定してください。", + "home_host": "これはCryptPadの独立コミュニティーのインスタンスです。", + "team_deleteConfirm": "チーム全体のデータを全て削除しようとしています。削除すると、チームの他のメンバーもデータにアクセスできなくなります。これは取り消せません。削除してよろしいですか?", + "team_kickConfirm": "{0}はチームから削除された旨通知されます。よろしいですか?", + "admin_instancePurposeHint": "インスタンスを運用する目的は何ですか?テレメトリーが有効になっている場合、回答は開発ロードマップに利用されます。", + "admin_purpose_experiment": "プラットフォームのテストもしくは新機能の開発", + "admin_purpose_noanswer": "回答しない", + "admin_instancePurposeTitle": "インスタンスの目的", + "requestEdit_request": "{1}がパッド({0})の編集を希望しています", + "support_from": "送信元: {0}", + "admin_supportInitPrivate": "あなたのCryptPadのインスタンスは、サポートメールボックスを使用するよう設定されていますが、アクセスに必要な秘密鍵がアカウントに登録されていません。以下のフォームから、秘密鍵を登録もしくは更新してください。", + "resources_imageBlocked": "リモート画像をブロックしました", + "team_inviteLinkErrorName": "招待する人の名前を追加してください。名前は後から変更できます。 ", + "snapshots_ooPickVersion": "スナップショットの作成には、対象となるバージョンの選択が必要です", + "notification_folderSharedTeam": "{0}がチーム({2})とフォルダ({1})を共有しました", + "notification_fileSharedTeam": "{0}がチーム({2})とファイル({1})を共有しました", + "notification_padSharedTeam": "{0}がチーム({2})とパッド({1})を共有しました", + "requestEdit_accepted": "{1}があなたにパッド({0})の編集権を付与しました", + "admin_registrationHint": "ユーザーの新規登録を許可しない", + "oo_deletedVersion": "このバージョンは履歴に存在しません。", + "team_pickFriends": "チームに招待する連絡先を選択", + "notification_fileShared": "{0}があなたとファイルを共有しました: {1}", + "resources_openInNewTab": "新しいタブで開く", + "download_zip": "ZIPファイルを作成しています…", + "fileTableHeader": "ダウンロードとアップロード", + "share_copyProfileLink": "プロフィールのリンクをコピー", + "calendar_weekNumber": "{0}週", + "admin_support_closed": "終了したチケット:", + "admin_support_answered": "回答済のチケット:", + "admin_support_normal": "未回答のチケット:", + "admin_limitNote": "メモ: {0}", + "share_linkFriends": "連絡先と共有", + "share_filterFriend": "名前で検索", + "notification_padShared": "{0}があなたとパッドを共有しました: {1}", + "profile_friendRequestSent": "連絡先のリクエストは保留中です…", + "friendRequest_notification": "{0}があなたに連絡先への追加のリクエストを送信しました", + "friendRequest_received": "{0}が連絡先への追加を希望しています", + "friendRequest_declined": "{0}が連絡先のリクエストを拒否しました", + "admin_diskUsageHint": "CryptPadのリソースが消費しているストレージ容量", + "timeoutError": "エラーが発生してサーバーへの接続が切断されました。
    Escキーを押してページを再読み込みしてください。", + "footer_team": "貢献者", + "admin_updateLimitHint": "ユーザーのストレージ制限の強制アップデートはいつでも可能です。エラーが発生したときは必須となります", + "admin_cat_general": "全般", + "poll_bookmarked_col": "列をブックマークしました。常に編集可能な仕方で左端に表示されます。", + "poll_bookmark_col": "この列をブックマークすると、常に編集可能な仕方で左端に表示されます", + "settings_colortheme_custom": "ユーザー定義", + "admin_getlimitsTitle": "個別の制限", + "admin_setlimitTitle": "個別の制限を適用", + "admin_setlimitHint": "公開鍵を使用して、ユーザーに個別の制限を設定します。既存のルールは変更または削除できます。", + "form_maxOptions": "{0}個まで回答可", + "form_anonymous_off": "ブロック", + "admin_checkupHint": "CryptPadには、一般的な構成に関する問題を自動で診断し、必要に応じてそれらを修正する方法を提案するページがあります。", + "admin_broadcastTitle": "メッセージを送信", + "admin_broadcastHint": "このインスタンスの全ユーザーにメッセージを送信します。ユーザーはメッセージを通知で受け取ります。送信前のメッセージは「通知をプレビュー」から確認できます。プレビューには赤いアイコンが付いており、プレビューはあなたの通知画面にのみ表示されます。", + "calendar_before": "前", + "share_formAuditor": "監査人", + "form_editMax": "選択できるオプションの最大数", + "form_invalidWarning": "回答にエラーがあります:", + "form_isOpen": "このフォームは公開中です", + "form_isClosed": "このフォームは{0}に閉じられました", + "form_willClose": "このフォームは{0}に終了します", + "settings_driveRedirectHint": "ログイン時にホームページからドライブに自動で転送する機能は、デフォルトで有効ではなくなりました。以前の動作は以下で有効にできます。", + "admin_removeDonateButtonLabel": "クラウドファンディングのキャンペーンの宣伝を表示しない", + "admin_purpose_public": "誰でも利用できるフリーのサービスを提供するため", + "owner_team_add": "{0}があなたをチーム({1})のオーナーにしようとしています。承諾しますか?", + "friendRequest_accepted": "{0}が連絡先リクエストを承諾しました", + "friendRequest_accept": "承諾(Enter)", + "admin_flushCacheHint": "サーバーから最新のコンテンツをダウンロードするようユーザーに強制(サーバーがフレッシュモードの場合のみ)", + "admin_activeSessionsHint": "アクティブなwebsocketの接続(および接続する一意のIPアドレス)の数", + "convertFolderToSF_SFParent": "このフォルダは別の共有フォルダ内にあるため、共有フォルダに変換できません。続行するには、外部に移動してください。", + "settings_deleteWarning": "注意:現在、プレミアムプランに加入しています(別のユーザーが支払いまたは提供)。アカウントを削除する前に、プランをキャンセルしてください。アカウントを削除すると、サポートに連絡しないとキャンセルできなくなります。", + "owner_request": "{0}はあなたを{1}のオーナーにしようとしています", + "owner_addConfirm": "共同オーナーは内容を変更したり、あなたをオーナーから削除したりすることができます。続行してよろしいですか?", + "download_zip_file": "ファイル {0}/{1}", + "documentID": "ドキュメントの識別子", + "admin_setlimitButton": "制限を設定", + "historyTrim_contentsSize": "内容: {0}", + "team_invitePasswordLoading": "招待状を復号化しています", + "team_pcsSelectLabel": "保存場所", + "admin_cat_broadcast": "お知らせ", + "docs_link": "ドキュメンテーション", + "admin_support_collapse": "折りたたむ", + "team_inviteFrom": "送信元:", + "admin_supportInitHint": "サポートのメールボックスを設定すると、あなたのCryptPadのインスタンスのユーザーが、アカウントから安全に問い合わせを行うことができるようになります。", + "isNotContact": "{0}はあなたの連絡先ではありません", + "isContact": "{0}はあなたの連絡先です", + "profile_info": "他のユーザーは、ドキュメントのユーザーリストであなたのアバターをクリックして、あなたのプロフィールを確認できます。", + "owner_removeMeConfirm": "オーナー権を放棄しようとしています。これは取り消せません。よろしいですか?", + "requestEdit_confirm": "{1}がパッド「{0}」の編集権を要求しました。編集権を与えますか?", + "admin_supportInitHelp": "サーバーはサポートメールボックスを使用するように設定されていません。サポートメールボックスを有効にし、ユーザーからメッセージを受け取るためには、サーバーの管理者に連絡し、「./scripts/generate-admin-keys.js」のスクリプトを実行してもらい、生成された公開鍵を「config.js」に保存して、秘密鍵をあなたに送信してもらうよう依頼する必要があります。", + "feedback_privacy": "私たちはプライバシーを配慮すると同時に、CryptPadを使いやすくしたいと望んでいます。このファイルは、実行されたアクションを特定するパラメーターと共に要求され、ユーザーにとって重要なUI機能を特定するために使用されます。", + "register_warning_note": "暗号化を行うCryptPadの性質上、サービス管理者は、ユーザー名とパスワードを忘れた場合にデータを回復することができません。ユーザー名とパスワードを安全な場所に保管してください。", + "history_restoreDriveTitle": "選択したバージョンのCryptDriveを復元", + "errorPopupBlocked": "新しいタブを開く許可が必要です。お使いのブラウザのアドレスバーから、ポップアップウィンドウを許可してください。これらのウィンドウが広告の表示に使用されることはありません。", + "settings_colorthemeHint": "このデバイスでのテーマ色を変更できます。", + "settings_colortheme_default": "システムのデフォルト ({0})", + "settings_codeBrackets": "括弧の自動補完", + "creation_helperText": "ドキュメンテーションを開く", + "contacts_info1": "連絡先の一覧が表示されます。ここでは以下を行うことができます:", + "share_noContactsLoggedIn": "連絡先がありません。プロフィールのリンクを共有して、連絡先のリクエストを送信してください。", + "settings_cat_security": "セキュリティー", + "whatis_collaboration_info": "

    CryptPadはコラボレーションを念頭に作られています。ドキュメントに加えられる変更は、リアルタイムで同期されます。全てのデータは暗号化されているため、サービスとその管理者が、編集され保存されているコンテンツを覗き見ることはできません。

    ", + "whatis_apps": "フルスイートアプリケーション", + "whatis_drive_info": "

    ドキュメントをCryptDriveに保存して管理できます。フォルダを作ったり共有したりできるほか、ドキュメントをタグ付けして整理することもできます。PDFファイル、写真、動画、音声などのファイルをアップロードして共有できます。チームのドライブを使うと、メンバーとデータを共有したり、ファイルの管理や、きめ細かいアクセス権のコントロールを行うことができます。

    ", + "whatis_model_info": "

    CryptPadは2016年より、寄付金とcryptpad.frの定額利用のほか、BPIフランス、NLNet財団、NGI Trust、Mozillaオープンソースサポートといったフランス、EUの研究助成を受けています。私たちは、公的資金で作られたソフトウェアについては、コードも公的に公開されるべきであると考えているため、サービスは全てオープンソースで提供しています。誰でも自由にこのソフトウェアを使用、運営、改変することができます。

    CryptPadは、ユーザーのデータを使って金銭上の利益を得ることはありません。これはプライバシーを尊重するオンラインサービスの展望の一部をなすものです。個人情報を使って金銭上の利益をあげながら「無料」を装う巨大プラットフォームとは違って、CryptPadは、ユーザーが自発的に支援を行うサービスのモデルを作ることを目指しています。

    私たちは、CryptPadのサービスを無料で提供しています。それは、金銭的に余裕のある人々だけでなく、誰もがプライバシーを得るに値すると確信しているからです。もしあなたがこのプロジェクトを支援できる状況にあるなら、機能の開発や、改良、メンテナンスに参加していただきたく考えます。そうすることで、全てのユーザーに利益をもたらされるはずです。

    このプロジェクトが実行可能であることが明らかになったいま、次のゴールは、ユーザーによる支援を通じて、プロジェクトを持続可能なものにすることにあります。CryptPadが、巨大プラットフォームに代わる持続可能なサービスを開発できるよう支援していただけるなら、一度もしくは継続的な寄付へのご協力をお願いいたします。

    ", + "whatis_xwiki_info": "

    CryptPadはフランス、パリに所在する会社であるXWikiにて開発されています。当社はオープンソース・ソフトウェアを15年以上にわたって開発しており、情報管理のための共同作業ソフトウェアの開発に関して豊富な経験をもっています。当社の実績は、私たちがCryptPadの長期的な開発と維持にコミットしていることを示しています。

    ", + "whatis_xwiki": "XWikiにて開発", + "history_restoreDrivePrompt": "CryptDriveの現在のバージョンを、表示されているバージョンに置き換えてよろしいですか?", + "owner_text": "パッドのオーナーは、オーナーの追加や削除、リストによるアクセス制限のほか、パッドの削除を行うことができます。", + "share_linkWarning": "このリンクは、ドキュメントを暗号化したり復号化したりする鍵を含んでいます。リンクを受け取った相手は、誰でも(ここにはコンピュータープログラムも含まれます)コンテンツにアクセスすることができます。", + "creation_expiresIn": "有効期限", + "settings_mediatagSizeHint": "ドキュメントに埋め込まれたメディア要素(画像、ビデオ、PDF)を自動で読み込む最大のサイズをメガバイト(MB)で指定してください。指定したサイズより大きい要素については、手動で読み込む必要があります。「-1」を設定すると、メディア要素は常に自動で読み込みます。", + "settings_mediatagSizeTitle": "自動ダウンロードの制限", + "settings_cacheHint": "CryptPadは、帯域の使用量を減らし、読み込み時間を短縮することを目的に、ドキュメントの各部分をブラウザ上のメモリに保存しています。もし空き容量が少なければ、キャッシュを無効にしてください。セキュリティー上の懸念から、キャッシュはログアウト時に毎回消去されますが、手動でキャッシュを消去して、デバイスの空き容量を確保することもできます。", + "team_infoContent": "それぞれのチームには、チーム所有のCryptDrive、ストレージのクォータ、チャット、メンバーリストが備わっています。チームのオーナーはチームを削除することができます。管理者はメンバーを招待したりキックしたりすることができます。メンバーは、チームを退会することができます。", + "home_support": "

    開発チームは、ユーザーのデータを使って金銭上の利益を得ることはありません。これはプライバシーを尊重するオンラインサービスの展望の一部をなすものです。個人情報を使って金銭上の利益をあげながら「無料」を装う巨大プラットフォームとは違って、CryptPadは、ユーザーが自発的に支援を行うサービスのモデルを作ることを目指しています。

    Open Collectiveから、一度もしくは継続的な寄付を行い、プロジェクトを支援していただくことができます。予算には透明性があり、更新については定期的に報告を行っています。お金によらずに貢献する方法も数多くあります。

    ", + "form_default": "ここに質問を入力", + "form_poll_switch": "行と列を入れ替える", + "settings_kanbanTagsHint": "複数のタグを選択したときにタグフィルターがどのように機能するかについて、「かつ」もしくは「または」から選択できます。「かつ」を選択すると、選択したタグを全て含むカードのみを表示し、「または」を選択すると、選択したタグのいずれかを含むカードを表示します。", + "notifications_cat_pads": "共有に関する通知", + "form_type_multicheck": "チェックボックス式グリッド", + "form_type_multiradio": "選択式グリッド", + "form_type_sort": "番号付きリスト", + "safeLinks_error": "このリンクはブラウザーのアドレスバーからコピーされており、ドキュメントにアクセスすることはできません。 共有メニューを使って、連絡先と直接ドキュメントを共有するか、リンクをコピーしてください。セーフリンク機能についてはこちらよりご確認ください。", + "team_cat_general": "チームについて", + "team_ownerConfirm": "共同オーナーはチームを変更もしくは削除したり、あなたをオーナーから削除したりすることができます。続行してよろしいですか?", + "owner_request_accepted": "{0}はあなたのリクエストを承諾し、「{1}」のオーナーになりました", + "team_acceptInvitation": "{0}がチーム「{1}」への招待を承諾しました", + "team_declineInvitation": "{0}がチーム「{1}」への招待を拒否しました", + "team_inviteLinkWarning": "このリンクにアクセスする最初の人は、このチームに参加して、コンテンツを閲覧することができます。リンクは慎重に共有してください。", + "team_inviteLinkTitle": "このチームへの招待状を好みに合わせて作成", + "team_inviteLinkTempName": "一時的な名前(招待の保留中リストに表示)", + "team_inviteLinkNoteMsg": "このメッセージは、チームに参加するかどうかを決める前に表示されます。", + "owner_removedPending": "{0}は「{1}」に関するオーナー権の申し出をキャンセルしました", + "team_pendingOwnerTitle": "この管理者はオーナー権の申し出を受諾していません。", + "team_rosterPromoteOwner": "オーナー権を提供", + "owner_removed": "{0}は「{1}」に関するあなたのオーナー権を削除しました", + "owner_request_declined": "{0}は「{1}」のオーナーになる依頼を拒否しました", + "team_demoteMeConfirm": "オーナー権を放棄しようとしています。これは取り消せません。よろしいですか?", + "share_noContactsNotLoggedIn": "ログインもしくは新規登録して、連絡先を確認するか新たに追加してください。", + "passwordFaqLink": "パスワードについて確認", + "share_embedPasswordAlert": "この項目はパスワードで保護されています。このパッドを埋め込む際、閲覧にはパスワードの入力が必要となります。", + "share_contactPasswordAlert": "この項目はパスワードで保護されています。CryptPadの連絡先と共有するため、相手がパスワードを入力する必要はありません。", + "share_linkPasswordAlert": "この項目はパスワードで保護されています。リンクを受け取った相手はパスワードを入力する必要があります。", + "register_notes_title": "重要な注意事項", + "teams_table_specificHint": "以前のバージョンの共有フォルダでは、閲覧者は既存のパッドを変更することができます。 これらのフォルダで作成またはコピーしたパッドには、標準の権限が与えられます。", + "broadcast_maintenance": "{0}から{1}の間でメンテナンスを予定しています。その間CryptPadは使用できません。", + "admin_archiveHint": "ドキュメントを完全削除することなく、利用できないよう設定できます。「アーカイブ」フォルダに移動し、数日後に削除します(期間については設定ファイルより設定できます)。", + "admin_unarchiveHint": "アーカイブされたドキュメントを復元できます", + "admin_registrationTitle": "登録を締め切る", + "admin_defaultlimitHint": "ユーザー定義のルールが適用されていない際の最大のストレージ容量(ユーザーとチームについて)を設定できます", + "admin_limit": "現在の制限: {0}", + "admin_getquotaHint": "ユーザーもしくはチームの公開鍵を入力して、設定されているクォータに対するストレージの使用量を確認できます。", + "admin_openFilesHint": "サーバー上で現在開いているファイル記述子の数", + "admin_support_premium": "プレミアムチケット:", + "admin_maintenanceHint": "このインスタンスのメンテナンスの日程を設定し、ユーザーに通知を送ることができます。メンテナンスの日程を複数設定することはできません。", + "admin_surveyHint": "外部サイトで行うアンケートのリンクを追加、更新、削除できます。ユーザーには通知が送信され、アンケートはユーザーのメニューから開くことができます。", + "broadcast_defaultLanguage": "この言語にフォールバック", + "admin_performanceProfilingHint": "サーバー側でコマンドを実行する際に掛かった合計時間の一覧です", + "admin_consentToContactHint": "サーバーのテレメトリーには、ソフトウェアや設定に関する重大な問題が発生した際に開発者が連絡できるよう、管理者の連絡先のメールアドレスが含まれます。メールアドレスが共有、売却、マーケティングの目的で使用されることはありません。サーバーに重大な問題が発生した際に連絡を希望する場合は、「同意する」にチェックを付けてください。", + "admin_removeDonateButtonHint": "CryptPadの開発の一部は、公的な助成金と寄付金によってまかなわれています。クラウドファンディングに関する宣伝をあなたのインスタンスで行うことで、開発者が万人のためにプラットフォームを改良することへの支援につながります。ただし、もし宣伝が不適切な場合は、宣伝を無効にすることもできます。", + "admin_performanceTimeHeading": "時間(秒)", + "team_maxTeams": "それぞれのユーザーアカウントがメンバーになることができるのは、最大{0}チームまでです。", + "team_listSlot": "利用可能なチームのスロット", + "resources_learnWhy": "ブロックした理由について", + "admin_purpose_business": "ビジネス・営利団体での使用", + "admin_purpose_education": "教育機関での使用", + "admin_purpose_org": "非営利団体・支援団体での使用", + "admin_purpose_personal": "個人利用、家族、友人との利用", + "burnAfterReading_linkBurnAfterReading": "一度だけ表示した後に自動で削除", + "feedback_about": "これを読んでいるのは、特定のアクションの実行時にCryptPadがウェブページのリクエストを送信している理由が気になるからだと思います。", + "burnAfterReading_warningLink": "パッドを自動削除に設定しました。リンクを受け取った相手がリンクを開くと、パッドは一度だけ表示され、その後削除されます。", + "burnAfterReading_warningAccess": "このドキュメントは自動的に削除されます。下のボタンをクリックするとコンテンツが自動的に表示され、その後で削除されます。ドキュメントを表示した後でウィンドウを閉じると、二度とドキュメントにアクセスすることはできません。準備ができていない場合は、ドキュメントを表示する前にこのウィンドウを閉じて、後ほどアクセスしてください。", + "form_sort_hint": "項目を優先順位(1が最も高く{0}が最も低い)に従って並べてください。", + "canvas_brush": "ペン", + "share_versionHash": "ドキュメントの選択したバージョンを閲覧モードで共有します。それにより、このドキュメントの全てのバージョンへの読み取り専用アクセスも可能になります。", + "history_cantRestore": "復元に失敗しました。接続が切れています。", + "todo_move": "タスクリストはCryptDriveのカンバン「{0}」に移動しました。", + "info_imprintFlavour": "このインスタンスの管理者に関する法的情報。", + "settings_safeLinkDefault": "セーフリンクは既定で有効になっています。ブラウザーのアドレスバーではなく、 共有メニューを使ってリンクをコピーしてください。", + "comments_notification": "{1}でのあなたのコメント「{0}」に対する返信", + "infobar_versionHash": "このドキュメントの過去のバージョンを閲覧しています({0})。", + "history_fastNext": "次の編集セッション", + "history_fastPrev": "以前の編集セッション", + "unableToDisplay": "ドキュメントを表示できません。Escキーを押してページの再読み込みを行ってください。問題が継続する場合は、サポートに連絡してください。", + "snapshots_cantMake": "スナップショットを作成できませんでした。接続が切れています。", + "snapshots_notFound": "ドキュメントの履歴が削除されたため、スナップショットは存在しません。", + "snapshot_error_exists": "このバージョンのスナップショットは既に存在します", + "calendar_dateTimeRange": "{0} {1} - {2}", + "toolbar_degraded": "現在{0}人以上がこのドキュメントを編集しています。パフォーマンスの改善のため、ユーザーリストとチャットを無効に設定しました。", + "oo_lostEdits": "新しいデータと同期したため、保存されていない編集箇所を復元することができませんでした。", + "pad_settings_comments": "コメントを既定で表示するか隠すかを選択してください。", + "pad_settings_outline": "目次を既定で表示するか隠すかを選択してください。", + "pad_settings_width_large": "最大幅", + "offlineError": "データを同期できないため、このページは現在表示できません。サービスへの接続が復旧すると読み込みを継続します。", + "share_noContactsOffline": "現在オフラインです。連絡先にアクセスすることはできません。", + "access_offline": "現在オフラインです。管理画面にアクセスすることはできません。", + "admin_support_last": "アップデートした日時: ", + "admin_support_first": "作成日時: ", + "contacts_confirmCancel": "{0}との連絡先リクエストをキャンセルしてよろしいですか?", + "history_trimPrompt": "このドキュメントは{0}の履歴があり、読み込みを遅くしている恐れがあります。必要でない場合は、履歴の削除を検討してください。", + "reminder_date": "{0}が{1}にあります", + "calendar_more": "あと{0}個", + "reminder_time": "{0}が今日の{1}にあります", + "form_answerWarning": "本人であることが確認されていません", + "team_leaveOwner": "チームから退出する前に、オーナーの役割から降格してください。チームには最低1人以上のオーナーが必要です。あなたが唯一のオーナーの場合は、別のオーナーを追加してください。" } diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index 477c993a1..29f415755 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -287,7 +287,7 @@ "fm_info_template": "Contains all the pads stored as templates and that you can re-use when you create a new pad.", "fm_info_recent": "These pads have recently been opened or modified by you or people you collaborate with.", "fm_info_trash": "Empty your trash to free space in your CryptDrive.", - "fm_info_anonymous": "You are not logged in so your documents will expire after {0} days. Clearing your browser's history may make them disappear.
    Sign up (no personal information required) or Log in to store them in your drive indefinitely. Read more about registered accounts.", + "fm_info_anonymous": "You are not logged in so your documents will expire after {0} days. Clearing your browser's history may make them disappear.
    Sign up (no personal information required) or Log in to store them in your drive indefinitely. Read more about registered accounts.", "fm_info_sharedFolder": "This is a shared folder. You're not logged in so you can only access it in read-only mode.
    Sign up or Log in to be able to import it to your CryptDrive and to modify it.", "fm_info_owned": "You are the owner of the pads displayed here. This means you can remove them permanently from the server whenever you want. If you do so, other users won't be able to access them anymore.", "fm_error_cantPin": "Internal server error. Please reload the page and try again.", @@ -1354,5 +1354,19 @@ "admin_provideAggregateStatisticsHint": "You may opt-in to providing additional usage metrics to the developers, such as the approximate number of registered and daily users for your instance.", "admin_provideAggregateStatisticsLabel": "Provide aggregated statistics", "form_poll_hint": ": Yes, : No, : Acceptable", - "fc_open_formro": "Open (as participant)" + "fc_open_formro": "Open (as participant)", + "resources_imageBlocked": "CryptPad blocked a remote image", + "resources_openInNewTab": "Open it in a new tab", + "resources_learnWhy": "Learn why it was blocked", + "admin_instancePurposeTitle": "Instance purpose", + "admin_purpose_noanswer": "I prefer not to say", + "admin_purpose_experiment": "To test the platform or develop new features", + "admin_purpose_personal": "For myself, family, or friends", + "admin_purpose_org": "For a non-profit organization or advocacy group", + "admin_purpose_education": "For a school, college, or university", + "admin_purpose_public": "To provide a free service to the public", + "admin_purpose_business": "For a business or commercial organization", + "admin_instancePurposeHint": "Why do you run this instance? Your answer will be used to inform the development roadmap if your telemetry is enabled.", + "team_leaveOwner": "Please demote yourself from the owner role before leaving the team. Note that teams must have at least one owner, please add one before proceeding if you are currently the only owner.", + "form_exportCSV": "Export to CSV" } diff --git a/www/common/translations/messages.nl.json b/www/common/translations/messages.nl.json index d9933fef6..393b91a05 100644 --- a/www/common/translations/messages.nl.json +++ b/www/common/translations/messages.nl.json @@ -414,7 +414,7 @@ "fm_viewListButton": "Lijstweergave", "fm_info_owned": "U bent de eigenaar van de werkomgevingen die hier zijn weergegeven. Dit betekent dat u ze voorgoed van de server kunt verwijderen. Als u dat doet, zijn ze niet meer toegankelijk voor andere gebruikers.", "fm_info_sharedFolder": "Dit is een gedeelde map. U bent niet ingelogd, dus u kunt de inhoud alleen lezen. U kunt zich
    registreren of inloggen om het in uw CryptDrive te importeren en het te wijzigen.", - "fm_info_anonymous": "U bent niet ingelogd, dus uw documenten zullen verlopen na {0} dagen. Ze kunnen ook verdwijnen als uw browsergeschiedenis gewist wordt.
    Registreren (geen persoonlijke gegevens vereist) of Inloggen om ze voor altijd in uw drive op te slaan. Lees meer over geregistreerde accounts.", + "fm_info_anonymous": "U bent niet ingelogd, dus uw documenten zullen verlopen na {0} dagen. Ze kunnen ook verdwijnen als uw browsergeschiedenis gewist wordt.
    Registreren (geen persoonlijke gegevens vereist) of Inloggen om ze voor altijd in uw drive op te slaan. Lees meer over geregistreerde accounts.", "fm_info_trash": "Leeg uw prullenbak om opslagruimte vrij te maken in uw CryptDrive.", "fm_info_recent": "Deze werkomgevingen zijn onlangs geopend of gewijzigd door u of door mensen met wie u samenwerkt.", "fm_info_template": "Dit bevat alle werkomgevingen die zijn opgeslagen als sjablonen en die je kunt gebruiken voor een nieuwe werkomgeving.", diff --git a/www/common/translations/messages.pt-br.json b/www/common/translations/messages.pt-br.json index 2a2f3dcd8..81237753d 100644 --- a/www/common/translations/messages.pt-br.json +++ b/www/common/translations/messages.pt-br.json @@ -9,7 +9,7 @@ "whiteboard": "Whiteboard", "file": "File", "media": "Media", - "kanban": "Kanban", + "kanban": "Placa de Assinatura", "todo": "A Fazer", "contacts": "Contactos", "sheet": "Planilha (Beta)", @@ -138,7 +138,7 @@ "fm_categoryError": "Incapaz de abrir a categoria selecionada, Exibindo diretório raiz", "fm_info_root": "Crie quantos diretórios aninhados aqui desejar para organizar seus arquivos..", "fm_info_trash": "Empty your trash to free space in your CryptDrive.", - "fm_info_anonymous": "Você não está logado, então estes documentos vão expirar em {0} dias. Limpar o histórico do seu navegador pode fazê-los desaparecer.
    Registre-se (nenhuma informação pessoal será requerida) ou Faça login para guarda-lo no seu disco. Leia mais sobre contas registradas.", + "fm_info_anonymous": "Você não está logado, então estes documentos vão expirar em {0} dias. Limpar o histórico do seu navegador pode fazê-los desaparecer.
    Registre-se (nenhuma informação pessoal será requerida) ou Faça login para guarda-lo no seu disco. Leia mais sobre contas registradas.", "fm_error_cantPin": "Erro interno do servidor. Por favor recarregue a página e tente novamente.", "fc_newfolder": "Nova pasta", "fc_rename": "Renomear", @@ -221,7 +221,7 @@ "header_logoTitle": "Go to the main page", "edit": "edit", "view": "view", - "feedback_about": "If you're reading this, you were probably curious why CryptPad is requesting web pages when you perform certain actions", + "feedback_about": "Se você está lendo isso, provavelmente está curioso para saber por que o CryptPad está solicitando páginas da web quando você executa certas ações.", "feedback_privacy": "We care about your privacy, and at the same time we want CryptPad to be very easy to use. We use this file to figure out which UI features matter to our users, by requesting it along with a parameter specifying which action was taken.", "feedback_optout": "If you would like to opt out, visit your user settings page, where you'll find a checkbox to enable or disable user feedback.", "button_newkanban": "Novo Kanban", @@ -842,7 +842,7 @@ "admin_flushCacheTitle": "Limpar cache HTTP", "settings_padNotifTitle": "Notificações de comentários", "comments_comment": "Comentário", - "comments_resolve": "Resolve", + "comments_resolve": "Resolver", "comments_reply": "Responder", "comments_submit": "Enviar", "comments_edited": "Editado", @@ -952,5 +952,9 @@ "contacts_unmute": "Com som", "contacts_mute": "Mudo", "share_noContactsNotLoggedIn": "Faça login ou registre-se para ver seus contatos existentes e adicionar novos.", - "share_copyProfileLink": "Copiar link do perfil" + "share_copyProfileLink": "Copiar link do perfil", + "settings_padOpenLinkTitle": "Forma", + "settings_padOpenLinkHint": "Com esta opção, você pode abrir links incorporados com um clique, sem o pop-up de visualização", + "settings_padOpenLinkLabel": "Habilitar abertura de link direto", + "settings_padNotifHint": "Ignorar notificações quando alguém responder a um de seus comentários" } diff --git a/www/common/translations/messages.ro.json b/www/common/translations/messages.ro.json index 0125606ae..0c0fe1f0e 100644 --- a/www/common/translations/messages.ro.json +++ b/www/common/translations/messages.ro.json @@ -118,7 +118,7 @@ "fm_info_root": "Crează câte foldere tip cuib ai nevoie pentru a-ți sorta fișierele.", "fm_info_template": "Conține toate pad-urile stocate ca șabloane și pe care le poți refolosi atunci când creezi un nou pad.", "fm_info_trash": "Fișierele șterse din gunoi vor fi șterse și din \"Toate fișierele\", făcând imposibilă recuperarea fișierelor din managerul de fișiere.", - "fm_info_anonymous": "Nu ești logat cu un cont valid așa că aceste pad-uri vor fi șterse (află de ce). Înscrie-te sau Loghează-te pentru a le salva.", + "fm_info_anonymous": "Nu ești logat cu un cont valid așa că aceste pad-uri vor fi șterse. Înscrie-te sau Loghează-te pentru a le salva.", "fc_newfolder": "Folder nou", "fc_rename": "Redenumește", "fc_open": "Deschide", diff --git a/www/common/translations/messages.ru.json b/www/common/translations/messages.ru.json index 8f08c3f0d..a46232761 100644 --- a/www/common/translations/messages.ru.json +++ b/www/common/translations/messages.ru.json @@ -299,7 +299,7 @@ "printCSS": "Пользовательские настройки вида (CSS):", "viewEmbedTag": "Чтобы встроить данный документ, вставьте iframe в нужную страницу. Вы можете настроить внешний вид используя CSS и HTML атрибуты.", "fm_ownedPadsName": "Собственный", - "fm_info_anonymous": "Вы не вошли в учетную запись, поэтому срок действия ваших пэдов истечет через 3 месяца (find out more). Они хранятся в вашем браузере, поэтому очистка истории может привести к их исчезновению..
    Sign up or Log in to keep them alive.
    ", + "fm_info_anonymous": "Вы не вошли в учетную запись, поэтому срок действия ваших пэдов истечет через 3 месяца. Они хранятся в вашем браузере, поэтому очистка истории может привести к их исчезновению..
    Sign up or Log in to keep them alive.", "fm_burnThisDriveButton": "Удалить всю информацию, хранящуюся от CryptPad в браузере", "fm_tags_used": "Количество использований", "fm_restoreDrive": "Восстановление прежнего состояния диска. Для достижения наилучших результатов не вносите изменения в диск, пока этот процесс не будет завершен.", diff --git a/www/common/translations/messages.th.json b/www/common/translations/messages.th.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/www/common/translations/messages.th.json @@ -0,0 +1 @@ +{} diff --git a/www/common/translations/messages.zh.json b/www/common/translations/messages.zh.json index 814d92cb3..49331f9a5 100644 --- a/www/common/translations/messages.zh.json +++ b/www/common/translations/messages.zh.json @@ -139,7 +139,7 @@ "fm_info_root": "在此建立任何巢狀目錄夾以便於整理分類你的檔案。", "fm_info_template": "包含所有工作檔案已存成模版,便於讓你在建立新工作檔案時套用。", "fm_info_trash": "清空垃圾筒好讓 CryptDrive 多出一些空間", - "fm_info_anonymous": "您没有登录,所以您的文档将在{0}天后过期。清除你的浏览器历史记录可能会使它们消失。
    注册 (无需个人信息) 或登录以无限期地把它们储存在你的硬盘里。 阅读更多关于注册帐户。", + "fm_info_anonymous": "您没有登录,所以您的文档将在{0}天后过期。清除你的浏览器历史记录可能会使它们消失。
    注册 (无需个人信息) 或登录以无限期地把它们储存在你的硬盘里。 阅读更多关于注册帐户。", "fm_error_cantPin": "內部伺服器出錯,請重新載入本頁並再試一次。", "fc_newfolder": "新資料夾", "fc_rename": "重新命名", diff --git a/www/convert/app-convert.less b/www/convert/app-convert.less new file mode 100644 index 000000000..c5813641a --- /dev/null +++ b/www/convert/app-convert.less @@ -0,0 +1,16 @@ +@import (reference) '../../customize/src/less2/include/framework.less'; +@import (reference) '../../customize/src/less2/include/sidebar-layout.less'; + +&.cp-app-convert { + + .framework_min_main( + @bg-color: @colortheme_apps[default], + ); + .sidebar-layout_main(); + + // body + display: flex; + flex-flow: column; + background-color: @cp_app-bg; + +} diff --git a/www/convert/index.html b/www/convert/index.html new file mode 100644 index 000000000..96a3cce86 --- /dev/null +++ b/www/convert/index.html @@ -0,0 +1,12 @@ + + + + CryptPad + + + + + + + + diff --git a/www/convert/inner.html b/www/convert/inner.html new file mode 100644 index 000000000..206b85722 --- /dev/null +++ b/www/convert/inner.html @@ -0,0 +1,18 @@ + + + + + + + + +
    +
    + + + diff --git a/www/convert/inner.js b/www/convert/inner.js new file mode 100644 index 000000000..cdcf745ca --- /dev/null +++ b/www/convert/inner.js @@ -0,0 +1,258 @@ +define([ + 'jquery', + '/api/config', + '/bower_components/chainpad-crypto/crypto.js', + '/common/toolbar.js', + '/bower_components/nthen/index.js', + '/common/sframe-common.js', + '/common/hyperscript.js', + '/customize/messages.js', + '/common/common-interface.js', + '/common/common-util.js', + + '/bower_components/file-saver/FileSaver.min.js', + 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', + 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', + 'less!/convert/app-convert.less', +], function ( + $, + ApiConfig, + Crypto, + Toolbar, + nThen, + SFCommon, + h, + Messages, + UI, + Util + ) +{ + var APP = {}; + + var common; + var sFrameChan; + + var debug = console.debug; + + var x2tReady = Util.mkEvent(true); + var x2tInitialized = false; + var x2tInit = function(x2t) { + debug("x2t mount"); + // x2t.FS.mount(x2t.MEMFS, {} , '/'); + x2t.FS.mkdir('/working'); + x2t.FS.mkdir('/working/media'); + x2t.FS.mkdir('/working/fonts'); + x2tInitialized = true; + x2tReady.fire(); + //fetchFonts(x2t); + debug("x2t mount done"); + }; + var getX2t = function (cb) { + 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"); + return void x2tReady.reg(function () { + cb(x2t); + }); + } + + x2t.onRuntimeInitialized = function() { + debug("x2t in runtime initialized"); + // Init x2t js module + x2tInit(x2t); + x2tReady.reg(function () { + cb(x2t); + }); + }; + }); + }; + /* + Converting Data + + This function converts a data in a specific format to the outputformat + The filename extension needs to represent the input format + Example: fileName=cryptpad.bin outputFormat=xlsx + */ + var getFormatId = function (ext) { + // Sheets + if (ext === 'xlsx') { return 257; } + if (ext === 'xls') { return 258; } + if (ext === 'ods') { return 259; } + if (ext === 'csv') { return 260; } + if (ext === 'pdf') { return 513; } + // Docs + if (ext === 'docx') { return 65; } + if (ext === 'doc') { return 66; } + if (ext === 'odt') { return 67; } + if (ext === 'txt') { return 69; } + if (ext === 'html') { return 70; } + + // Slides + if (ext === 'pptx') { return 129; } + if (ext === 'ppt') { return 130; } + if (ext === 'odp') { return 131; } + + return; + }; + var getFromId = function (ext) { + var id = getFormatId(ext); + if (!id) { return ''; } + return ''+id+''; + }; + var getToId = function (ext) { + var id = getFormatId(ext); + if (!id) { return ''; } + return ''+id+''; + }; + var x2tConvertDataInternal = function(x2t, data, fileName, outputFormat) { + debug("Converting Data for " + fileName + " to " + outputFormat); + + var inputFormat = fileName.split('.').pop(); + + x2t.FS.writeFile('/working/' + fileName, data); + var params = "" + + "" + + "/working/" + fileName + "" + + "/working/" + fileName + "." + outputFormat + "" + + getFromId(inputFormat) + + getToId(outputFormat) + + "false" + + ""; + // writing params file to mounted working disk (in memory) + x2t.FS.writeFile('/working/params.xml', params); + // running conversion + x2t.ccall("runX2T", ["number"], ["string"], ["/working/params.xml"]); + // reading output file from working disk (in memory) + var result; + try { + result = x2t.FS.readFile('/working/' + fileName + "." + outputFormat); + } catch (e) { + console.error(e, x2t.FS); + debug("Failed reading converted file"); + UI.warn(Messages.error); + return ""; + } + return result; + }; + var x2tConverter = function (typeSrc, typeTarget) { + return function (data, name, cb) { + getX2t(function (x2t) { + if (typeSrc === 'ods') { + data = x2tConvertDataInternal(x2t, data, name, 'xlsx'); + name += '.xlsx'; + } + if (typeSrc === 'odt') { + data = x2tConvertDataInternal(x2t, data, name, 'docx'); + name += '.docx'; + } + if (typeSrc === 'odp') { + data = x2tConvertDataInternal(x2t, data, name, 'pptx'); + name += '.pptx'; + } + cb(x2tConvertDataInternal(x2t, data, name, typeTarget)); + }); + }; + }; + + var CONVERTERS = { + xlsx: { + //pdf: x2tConverter('xlsx', 'pdf'), + ods: x2tConverter('xlsx', 'ods'), + bin: x2tConverter('xlsx', 'bin'), + }, + ods: { + //pdf: x2tConverter('ods', 'pdf'), + xlsx: x2tConverter('ods', 'xlsx'), + bin: x2tConverter('ods', 'bin'), + }, + odt: { + docx: x2tConverter('odt', 'docx'), + txt: x2tConverter('odt', 'txt'), + bin: x2tConverter('odt', 'bin'), + }, + docx: { + odt: x2tConverter('docx', 'odt'), + txt: x2tConverter('docx', 'txt'), + bin: x2tConverter('docx', 'bin'), + }, + txt: { + odt: x2tConverter('txt', 'odt'), + docx: x2tConverter('txt', 'docx'), + bin: x2tConverter('txt', 'bin'), + }, + odp: { + pptx: x2tConverter('odp', 'pptx'), + bin: x2tConverter('odp', 'bin'), + }, + pptx: { + odp: x2tConverter('pptx', 'odp'), + bin: x2tConverter('pptx', 'bin'), + }, + }; + + Messages.convertPage = "Convert"; // XXX + Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterward."; // XXX + + var createToolbar = function () { + var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications']; + var configTb = { + displayed: displayed, + sfCommon: common, + $container: APP.$toolbar, + pageTitle: Messages.convertPage, + metadataMgr: common.getMetadataMgr(), + }; + APP.toolbar = Toolbar.create(configTb); + APP.toolbar.$rightside.hide(); + }; + + nThen(function (waitFor) { + $(waitFor(UI.addLoadingScreen)); + SFCommon.create(waitFor(function (c) { APP.common = common = c; })); + }).nThen(function (waitFor) { + APP.$container = $('#cp-sidebarlayout-container'); + APP.$toolbar = $('#cp-toolbar'); + APP.$leftside = $('
    ', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container); + APP.$rightside = $('
    ', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container); + sFrameChan = common.getSframeChannel(); + sFrameChan.onReady(waitFor()); + }).nThen(function (/*waitFor*/) { + createToolbar(); + + var hint = h('p.cp-convert-hint', Messages.convert_hint); + + var picker = h('input', { + type: 'file' + }); + APP.$rightside.append([hint, picker]); + + $(picker).on('change', function () { + var file = picker.files[0]; + var name = file && file.name; + var reader = new FileReader(); + var parsed = file && file.name && /.+\.([^.]+)$/.exec(file.name); + var ext = parsed && parsed[1]; + reader.onload = function (e) { + if (CONVERTERS[ext]) { + Object.keys(CONVERTERS[ext]).forEach(function (to) { + var button = h('button.btn', to); + $(button).click(function () { + CONVERTERS[ext][to](new Uint8Array(e.target.result), name, function (a) { + var n = name.slice(0, -ext.length) + to; + var blob = new Blob([a], {type: "application/bin;charset=utf-8"}); + window.saveAs(blob, n); + }); + + }).appendTo(APP.$rightside); + }); + } + }; + reader.readAsArrayBuffer(file, 'application/octet-stream'); + }); + + UI.removeLoadingScreen(); + + }); +}); diff --git a/www/convert/main.js b/www/convert/main.js new file mode 100644 index 000000000..236290b13 --- /dev/null +++ b/www/convert/main.js @@ -0,0 +1,28 @@ +// Load #1, load as little as possible because we are in a race to get the loading screen up. +define([ + '/bower_components/nthen/index.js', + '/api/config', + '/common/dom-ready.js', + '/common/sframe-common-outer.js' +], function (nThen, ApiConfig, DomReady, SFCommonO) { + + // Loaded in load #2 + nThen(function (waitFor) { + DomReady.onReady(waitFor()); + }).nThen(function (waitFor) { + SFCommonO.initIframe(waitFor, true); + }).nThen(function (/*waitFor*/) { + var category; + if (window.location.hash) { + category = window.location.hash.slice(1); + window.location.hash = ''; + } + var addData = function (obj) { + if (category) { obj.category = category; } + }; + SFCommonO.start({ + noRealtime: true, + addData: addData + }); + }); +}); diff --git a/www/form/app-form.less b/www/form/app-form.less index 433316448..95802e18b 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -470,7 +470,11 @@ //padding: 10px; } + .cp-form-creator-results-export { + margin-bottom: 20px; + } .cp-form-creator-results-content { + padding-bottom: 100px; .cp-form-block { background: @cp_form-bg1; padding: 10px; diff --git a/www/form/export.js b/www/form/export.js new file mode 100644 index 000000000..4afc4170a --- /dev/null +++ b/www/form/export.js @@ -0,0 +1,72 @@ +define([ + '/common/common-util.js', + '/customize/messages.js' +], function (Util, Messages) { + var Export = {}; + + var escapeCSV = function (v) { + if (!/("|,|\n|;)/.test(v)) { + return v || ''; + } + var value = ''; + var vv = (v || '').replaceAll('"', '""'); + value += '"' + vv + '"'; + return value; + }; + Export.results = function (content, answers, TYPES) { + if (!content || !content.form) { return; } + var csv = ""; + var form = content.form; + + var questions = [Messages.form_poll_time, Messages.share_formView]; + + content.order.forEach(function (key) { + var obj = form[key]; + if (!obj) { return; } + var type = obj.type; + if (!TYPES[type]) { return; } // Ignore static types + var c; + if (TYPES[type] && TYPES[type].exportCSV) { c = TYPES[type].exportCSV(false, obj); } + if (!c) { c = [obj.q || Messages.form_default]; } + Array.prototype.push.apply(questions, c); + }); + + questions.forEach(function (v, i) { + if (i) { csv += ','; } + csv += escapeCSV(v); + }); + + Object.keys(answers || {}).forEach(function (key) { + var obj = answers[key]; + csv += '\n'; + var time = new Date(obj.time).toISOString(); + var msg = obj.msg || {}; + var user = msg._userdata || {}; + csv += escapeCSV(time); + csv += ',' + escapeCSV(user.name || Messages.anonymous); + content.order.forEach(function (key) { + var type = form[key].type; + if (!TYPES[type]) { return; } // Ignore static types + if (TYPES[type].exportCSV) { + var res = TYPES[type].exportCSV(msg[key], form[key]).map(function (str) { + return escapeCSV(str); + }).join(','); + csv += ',' + res; + return; + } + csv += ',' + escapeCSV(String(msg[key] || '')); + }); + }); + return csv; + }; + + Export.main = function (content, cb) { + var json = Util.clone(content || {}); + delete json.answers; + cb(new Blob([JSON.stringify(json, 0, 2)], { + type: 'application/json;charset=utf-8' + })); + }; + + return Export; +}); diff --git a/www/form/inner.js b/www/form/inner.js index 80c50a0d6..2a1501ecb 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -4,6 +4,7 @@ define([ '/bower_components/chainpad-crypto/crypto.js', '/common/sframe-app-framework.js', '/common/toolbar.js', + '/form/export.js', '/bower_components/nthen/index.js', '/common/sframe-common.js', '/common/common-util.js', @@ -30,6 +31,8 @@ define([ 'cm/mode/gfm/gfm', 'css!cm/lib/codemirror.css', + '/bower_components/file-saver/FileSaver.min.js', + 'css!/bower_components/codemirror/lib/codemirror.css', 'css!/bower_components/codemirror/addon/dialog/dialog.css', 'css!/bower_components/codemirror/addon/fold/foldgutter.css', @@ -42,6 +45,7 @@ define([ Crypto, Framework, Toolbar, + Exporter, nThen, SFCommon, Util, @@ -76,8 +80,11 @@ define([ timeFormat = "h:i K"; } - var MAX_OPTIONS = 15; // XXX - var MAX_ITEMS = 10; // XXX + // multi-line radio, checkboxes, and possibly other things have a max number of items + // we'll consider increasing this restriction if people are unhappy with it + // but as a general rule we expect users will appreciate having simpler questions + var MAX_OPTIONS = 15; + var MAX_ITEMS = 10; var saveAndCancelOptions = function (getRes, cb) { // Cancel changes @@ -1208,6 +1215,20 @@ define([ return h('div.cp-form-results-type-radio', results); }, + exportCSV: function (answer, form) { + var opts = form.opts || {}; + var q = form.q || Messages.form_default; + if (answer === false) { + return (opts.items || []).map(function (obj) { + return q + ' | ' + obj.v; + }); + } + if (!answer) { return ['']; } + return (opts.items || []).map(function (obj) { + var uid = obj.uid; + return String(answer[uid] || ''); + }); + }, icon: h('i.cptools.cptools-form-grid-radio') }, checkbox: { @@ -1422,6 +1443,20 @@ define([ return h('div.cp-form-results-type-radio', results); }, + exportCSV: function (answer, form) { + var opts = form.opts || {}; + var q = form.q || Messages.form_default; + if (answer === false) { + return (opts.items || []).map(function (obj) { + return q + ' | ' + obj.v; + }); + } + if (!answer) { return ['']; } + return (opts.items || []).map(function (obj) { + var uid = obj.uid; + return String(answer[uid] || ''); + }); + }, icon: h('i.cptools.cptools-form-grid-check') }, sort: { @@ -1642,6 +1677,16 @@ define([ return h('div.cp-form-type-poll', lines); }, + exportCSV: function (answer) { + if (answer === false) { return; } + if (!answer || !answer.values) { return ['']; } + var str = ''; + Object.keys(answer.values).sort().forEach(function (k, i) { + if (i !== 0) { str += ';'; } + str += k.replace(';', '').replace(':', '') + ':' + answer.values[k]; + }); + return [str]; + }, icon: h('i.cptools.cptools-form-poll') }, }; @@ -1656,9 +1701,21 @@ define([ var controls = h('div.cp-form-creator-results-controls'); var $controls = $(controls).appendTo($container); + var exportButton = h('button.btn.btn-secondary', Messages.exportButton); // XXX form_exportCSV; + var exportCSV = h('div.cp-form-creator-results-export', exportButton); + $(exportCSV).appendTo($container); var results = h('div.cp-form-creator-results-content'); var $results = $(results).appendTo($container); + $(exportButton).click(function () { + var csv = Exporter.results(content, answers, TYPES); + if (!csv) { return void UI.warn(Messages.error); } + var suggestion = APP.framework._.title.suggestTitle('cryptpad-document'); + var title = Util.fixFileName(suggestion) + '.csv'; + window.saveAs(new Blob([csv], { + type: 'text/csv' + }), title); + }); var summary = true; var form = content.form; @@ -2315,6 +2372,7 @@ define([ var andThen = function (framework) { framework.start(); + APP.framework = framework; var evOnChange = Util.mkEvent(); var content = {}; @@ -2739,6 +2797,17 @@ define([ return content; }); + framework.setFileImporter({ accept: ['.json'] }, function (newContent) { + var parsed = JSON.parse(newContent || {}); + parsed.answers = content.answers; + return parsed; + }); + + framework.setFileExporter(['.json'], function(cb, ext) { + Exporter.main(content, cb, ext); + }, true); + + }; Framework.create({ diff --git a/www/form/templates.js b/www/form/templates.js new file mode 100644 index 000000000..6ff3249b8 --- /dev/null +++ b/www/form/templates.js @@ -0,0 +1,20 @@ +define([ + '/customize/messages.js' +], function (Messages) { + return [{ + id: 'a', + used: 1, + name: Messages.form_type_poll, + content: { + form: { + "1": { + type: 'md' + }, + "2": { + type: 'poll' + } + }, + order: ["1", "2"] + } + }]; +}); diff --git a/www/kanban/export.js b/www/kanban/export.js index 9ee770ac7..694724cf4 100644 --- a/www/kanban/export.js +++ b/www/kanban/export.js @@ -13,6 +13,73 @@ define([ })); }; + module.import = function (content) { + // Import from Trello + + var c = { + data: {}, + items: {}, + list: [] + }; + + var colorMap = { + red: 'color1', + orange: 'color2', + yellow: 'color3', + lime: 'color4', + green: 'color5', + sky: 'color6', + blue: 'color7', + purple: 'color8', + pink: 'color9', + black: 'nocolor' + }; + content.cards.forEach(function (obj, i) { + var tags; + var color; + if (Array.isArray(obj.labels)) { + obj.labels.forEach(function (l) { + if (!color) { + color = colorMap[l.color] || ''; + } + if (l.name) { + tags = tags || []; + var n = l.name.toLowerCase().trim(); + if (tags.indexOf(n) === -1) { tags.push(n); } + } + }); + } + c.items[(i+1)] = { + id: (i+1), + title: obj.name, + body: obj.desc, + color: color, + tags: tags + }; + }); + + var id = 1; + content.lists.forEach(function (obj) { + var _id = obj.id; + var cards = []; + content.cards.forEach(function (card, i) { + if (card.idList === _id) { + cards.push(i+1); + } + }); + c.data[id] = { + id: id, + title: obj.name, + item: cards + }; + c.list.push(id); + + id++; + }); + + return c; + }; + return module; }); diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 76a9fe1cc..4832adcca 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -18,6 +18,7 @@ define([ '/bower_components/marked/marked.min.js', 'cm/lib/codemirror', '/kanban/jkanban_cp.js', + '/kanban/export.js', 'cm/mode/gfm/gfm', 'cm/addon/edit/closebrackets', @@ -50,7 +51,8 @@ define([ ChainPad, Marked, CodeMirror, - jKanban) + jKanban, + Export) { var verbose = function (x) { console.log(x); }; @@ -287,7 +289,7 @@ define([ var fileHost = privateData.fileHost || privateData.origin; var src = fileHost + Hash.getBlobPathFromHex(secret.channel); var key = Hash.encodeBase64(secret.keys.cryptKey); - var mt = ''; + var mt = UI.mediaTag(src, key).outerHTML; editor.replaceSelection(mt); } }; @@ -1060,6 +1062,11 @@ define([ var parsed; try { parsed = JSON.parse(content); } catch (e) { return void console.error(e); } + + if (parsed && parsed.id && parsed.lists && parsed.cards) { + return { content: Export.import(parsed) }; + } + return { content: parsed }; }); diff --git a/www/lib/changelog.md b/www/lib/changelog.md new file mode 100644 index 000000000..86ef6ec22 --- /dev/null +++ b/www/lib/changelog.md @@ -0,0 +1,4 @@ +This file is intended to be used as a log of what third-party source we have vendored, where we got it, and what modifications we have made to it (if any). + +* [turndown v7.1.1](https://github.com/mixmark-io/turndown/releases/tag/v7.1.1) built from unmodified source as per its build scripts. + diff --git a/www/lib/turndown.browser.umd.js b/www/lib/turndown.browser.umd.js new file mode 100644 index 000000000..a812101ae --- /dev/null +++ b/www/lib/turndown.browser.umd.js @@ -0,0 +1,976 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.TurndownService = factory()); +}(this, (function () { 'use strict'; + + function extend (destination) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + for (var key in source) { + if (source.hasOwnProperty(key)) destination[key] = source[key]; + } + } + return destination + } + + function repeat (character, count) { + return Array(count + 1).join(character) + } + + function trimLeadingNewlines (string) { + return string.replace(/^\n*/, '') + } + + function trimTrailingNewlines (string) { + // avoid match-at-end regexp bottleneck, see #370 + var indexEnd = string.length; + while (indexEnd > 0 && string[indexEnd - 1] === '\n') indexEnd--; + return string.substring(0, indexEnd) + } + + var blockElements = [ + 'ADDRESS', 'ARTICLE', 'ASIDE', 'AUDIO', 'BLOCKQUOTE', 'BODY', 'CANVAS', + 'CENTER', 'DD', 'DIR', 'DIV', 'DL', 'DT', 'FIELDSET', 'FIGCAPTION', 'FIGURE', + 'FOOTER', 'FORM', 'FRAMESET', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER', + 'HGROUP', 'HR', 'HTML', 'ISINDEX', 'LI', 'MAIN', 'MENU', 'NAV', 'NOFRAMES', + 'NOSCRIPT', 'OL', 'OUTPUT', 'P', 'PRE', 'SECTION', 'TABLE', 'TBODY', 'TD', + 'TFOOT', 'TH', 'THEAD', 'TR', 'UL' + ]; + + function isBlock (node) { + return is(node, blockElements) + } + + var voidElements = [ + 'AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', + 'KEYGEN', 'LINK', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR' + ]; + + function isVoid (node) { + return is(node, voidElements) + } + + function hasVoid (node) { + return has(node, voidElements) + } + + var meaningfulWhenBlankElements = [ + 'A', 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TH', 'TD', 'IFRAME', 'SCRIPT', + 'AUDIO', 'VIDEO' + ]; + + function isMeaningfulWhenBlank (node) { + return is(node, meaningfulWhenBlankElements) + } + + function hasMeaningfulWhenBlank (node) { + return has(node, meaningfulWhenBlankElements) + } + + function is (node, tagNames) { + return tagNames.indexOf(node.nodeName) >= 0 + } + + function has (node, tagNames) { + return ( + node.getElementsByTagName && + tagNames.some(function (tagName) { + return node.getElementsByTagName(tagName).length + }) + ) + } + + var rules = {}; + + rules.paragraph = { + filter: 'p', + + replacement: function (content) { + return '\n\n' + content + '\n\n' + } + }; + + rules.lineBreak = { + filter: 'br', + + replacement: function (content, node, options) { + return options.br + '\n' + } + }; + + rules.heading = { + filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + + replacement: function (content, node, options) { + var hLevel = Number(node.nodeName.charAt(1)); + + if (options.headingStyle === 'setext' && hLevel < 3) { + var underline = repeat((hLevel === 1 ? '=' : '-'), content.length); + return ( + '\n\n' + content + '\n' + underline + '\n\n' + ) + } else { + return '\n\n' + repeat('#', hLevel) + ' ' + content + '\n\n' + } + } + }; + + rules.blockquote = { + filter: 'blockquote', + + replacement: function (content) { + content = content.replace(/^\n+|\n+$/g, ''); + content = content.replace(/^/gm, '> '); + return '\n\n' + content + '\n\n' + } + }; + + rules.list = { + filter: ['ul', 'ol'], + + replacement: function (content, node) { + var parent = node.parentNode; + if (parent.nodeName === 'LI' && parent.lastElementChild === node) { + return '\n' + content + } else { + return '\n\n' + content + '\n\n' + } + } + }; + + rules.listItem = { + filter: 'li', + + replacement: function (content, node, options) { + content = content + .replace(/^\n+/, '') // remove leading newlines + .replace(/\n+$/, '\n') // replace trailing newlines with just a single one + .replace(/\n/gm, '\n '); // indent + var prefix = options.bulletListMarker + ' '; + var parent = node.parentNode; + if (parent.nodeName === 'OL') { + var start = parent.getAttribute('start'); + var index = Array.prototype.indexOf.call(parent.children, node); + prefix = (start ? Number(start) + index : index + 1) + '. '; + } + return ( + prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '') + ) + } + }; + + rules.indentedCodeBlock = { + filter: function (node, options) { + return ( + options.codeBlockStyle === 'indented' && + node.nodeName === 'PRE' && + node.firstChild && + node.firstChild.nodeName === 'CODE' + ) + }, + + replacement: function (content, node, options) { + return ( + '\n\n ' + + node.firstChild.textContent.replace(/\n/g, '\n ') + + '\n\n' + ) + } + }; + + rules.fencedCodeBlock = { + filter: function (node, options) { + return ( + options.codeBlockStyle === 'fenced' && + node.nodeName === 'PRE' && + node.firstChild && + node.firstChild.nodeName === 'CODE' + ) + }, + + replacement: function (content, node, options) { + var className = node.firstChild.getAttribute('class') || ''; + var language = (className.match(/language-(\S+)/) || [null, ''])[1]; + var code = node.firstChild.textContent; + + var fenceChar = options.fence.charAt(0); + var fenceSize = 3; + var fenceInCodeRegex = new RegExp('^' + fenceChar + '{3,}', 'gm'); + + var match; + while ((match = fenceInCodeRegex.exec(code))) { + if (match[0].length >= fenceSize) { + fenceSize = match[0].length + 1; + } + } + + var fence = repeat(fenceChar, fenceSize); + + return ( + '\n\n' + fence + language + '\n' + + code.replace(/\n$/, '') + + '\n' + fence + '\n\n' + ) + } + }; + + rules.horizontalRule = { + filter: 'hr', + + replacement: function (content, node, options) { + return '\n\n' + options.hr + '\n\n' + } + }; + + rules.inlineLink = { + filter: function (node, options) { + return ( + options.linkStyle === 'inlined' && + node.nodeName === 'A' && + node.getAttribute('href') + ) + }, + + replacement: function (content, node) { + var href = node.getAttribute('href'); + var title = cleanAttribute(node.getAttribute('title')); + if (title) title = ' "' + title + '"'; + return '[' + content + '](' + href + title + ')' + } + }; + + rules.referenceLink = { + filter: function (node, options) { + return ( + options.linkStyle === 'referenced' && + node.nodeName === 'A' && + node.getAttribute('href') + ) + }, + + replacement: function (content, node, options) { + var href = node.getAttribute('href'); + var title = cleanAttribute(node.getAttribute('title')); + if (title) title = ' "' + title + '"'; + var replacement; + var reference; + + switch (options.linkReferenceStyle) { + case 'collapsed': + replacement = '[' + content + '][]'; + reference = '[' + content + ']: ' + href + title; + break + case 'shortcut': + replacement = '[' + content + ']'; + reference = '[' + content + ']: ' + href + title; + break + default: + var id = this.references.length + 1; + replacement = '[' + content + '][' + id + ']'; + reference = '[' + id + ']: ' + href + title; + } + + this.references.push(reference); + return replacement + }, + + references: [], + + append: function (options) { + var references = ''; + if (this.references.length) { + references = '\n\n' + this.references.join('\n') + '\n\n'; + this.references = []; // Reset references + } + return references + } + }; + + rules.emphasis = { + filter: ['em', 'i'], + + replacement: function (content, node, options) { + if (!content.trim()) return '' + return options.emDelimiter + content + options.emDelimiter + } + }; + + rules.strong = { + filter: ['strong', 'b'], + + replacement: function (content, node, options) { + if (!content.trim()) return '' + return options.strongDelimiter + content + options.strongDelimiter + } + }; + + rules.code = { + filter: function (node) { + var hasSiblings = node.previousSibling || node.nextSibling; + var isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings; + + return node.nodeName === 'CODE' && !isCodeBlock + }, + + replacement: function (content) { + if (!content) return '' + content = content.replace(/\r?\n|\r/g, ' '); + + var extraSpace = /^`|^ .*?[^ ].* $|`$/.test(content) ? ' ' : ''; + var delimiter = '`'; + var matches = content.match(/`+/gm) || []; + while (matches.indexOf(delimiter) !== -1) delimiter = delimiter + '`'; + + return delimiter + extraSpace + content + extraSpace + delimiter + } + }; + + rules.image = { + filter: 'img', + + replacement: function (content, node) { + var alt = cleanAttribute(node.getAttribute('alt')); + var src = node.getAttribute('src') || ''; + var title = cleanAttribute(node.getAttribute('title')); + var titlePart = title ? ' "' + title + '"' : ''; + return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : '' + } + }; + + function cleanAttribute (attribute) { + return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '' + } + + /** + * Manages a collection of rules used to convert HTML to Markdown + */ + + function Rules (options) { + this.options = options; + this._keep = []; + this._remove = []; + + this.blankRule = { + replacement: options.blankReplacement + }; + + this.keepReplacement = options.keepReplacement; + + this.defaultRule = { + replacement: options.defaultReplacement + }; + + this.array = []; + for (var key in options.rules) this.array.push(options.rules[key]); + } + + Rules.prototype = { + add: function (key, rule) { + this.array.unshift(rule); + }, + + keep: function (filter) { + this._keep.unshift({ + filter: filter, + replacement: this.keepReplacement + }); + }, + + remove: function (filter) { + this._remove.unshift({ + filter: filter, + replacement: function () { + return '' + } + }); + }, + + forNode: function (node) { + if (node.isBlank) return this.blankRule + var rule; + + if ((rule = findRule(this.array, node, this.options))) return rule + if ((rule = findRule(this._keep, node, this.options))) return rule + if ((rule = findRule(this._remove, node, this.options))) return rule + + return this.defaultRule + }, + + forEach: function (fn) { + for (var i = 0; i < this.array.length; i++) fn(this.array[i], i); + } + }; + + function findRule (rules, node, options) { + for (var i = 0; i < rules.length; i++) { + var rule = rules[i]; + if (filterValue(rule, node, options)) return rule + } + return void 0 + } + + function filterValue (rule, node, options) { + var filter = rule.filter; + if (typeof filter === 'string') { + if (filter === node.nodeName.toLowerCase()) return true + } else if (Array.isArray(filter)) { + if (filter.indexOf(node.nodeName.toLowerCase()) > -1) return true + } else if (typeof filter === 'function') { + if (filter.call(rule, node, options)) return true + } else { + throw new TypeError('`filter` needs to be a string, array, or function') + } + } + + /** + * The collapseWhitespace function is adapted from collapse-whitespace + * by Luc Thevenard. + * + * The MIT License (MIT) + * + * Copyright (c) 2014 Luc Thevenard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + /** + * collapseWhitespace(options) removes extraneous whitespace from an the given element. + * + * @param {Object} options + */ + function collapseWhitespace (options) { + var element = options.element; + var isBlock = options.isBlock; + var isVoid = options.isVoid; + var isPre = options.isPre || function (node) { + return node.nodeName === 'PRE' + }; + + if (!element.firstChild || isPre(element)) return + + var prevText = null; + var keepLeadingWs = false; + + var prev = null; + var node = next(prev, element, isPre); + + while (node !== element) { + if (node.nodeType === 3 || node.nodeType === 4) { // Node.TEXT_NODE or Node.CDATA_SECTION_NODE + var text = node.data.replace(/[ \r\n\t]+/g, ' '); + + if ((!prevText || / $/.test(prevText.data)) && + !keepLeadingWs && text[0] === ' ') { + text = text.substr(1); + } + + // `text` might be empty at this point. + if (!text) { + node = remove(node); + continue + } + + node.data = text; + + prevText = node; + } else if (node.nodeType === 1) { // Node.ELEMENT_NODE + if (isBlock(node) || node.nodeName === 'BR') { + if (prevText) { + prevText.data = prevText.data.replace(/ $/, ''); + } + + prevText = null; + keepLeadingWs = false; + } else if (isVoid(node) || isPre(node)) { + // Avoid trimming space around non-block, non-BR void elements and inline PRE. + prevText = null; + keepLeadingWs = true; + } else if (prevText) { + // Drop protection if set previously. + keepLeadingWs = false; + } + } else { + node = remove(node); + continue + } + + var nextNode = next(prev, node, isPre); + prev = node; + node = nextNode; + } + + if (prevText) { + prevText.data = prevText.data.replace(/ $/, ''); + if (!prevText.data) { + remove(prevText); + } + } + } + + /** + * remove(node) removes the given node from the DOM and returns the + * next node in the sequence. + * + * @param {Node} node + * @return {Node} node + */ + function remove (node) { + var next = node.nextSibling || node.parentNode; + + node.parentNode.removeChild(node); + + return next + } + + /** + * next(prev, current, isPre) returns the next node in the sequence, given the + * current and previous nodes. + * + * @param {Node} prev + * @param {Node} current + * @param {Function} isPre + * @return {Node} + */ + function next (prev, current, isPre) { + if ((prev && prev.parentNode === current) || isPre(current)) { + return current.nextSibling || current.parentNode + } + + return current.firstChild || current.nextSibling || current.parentNode + } + + /* + * Set up window for Node.js + */ + + var root = (typeof window !== 'undefined' ? window : {}); + + /* + * Parsing HTML strings + */ + + function canParseHTMLNatively () { + var Parser = root.DOMParser; + var canParse = false; + + // Adapted from https://gist.github.com/1129031 + // Firefox/Opera/IE throw errors on unsupported types + try { + // WebKit returns null on unsupported types + if (new Parser().parseFromString('', 'text/html')) { + canParse = true; + } + } catch (e) {} + + return canParse + } + + function createHTMLParser () { + var Parser = function () {}; + + { + if (shouldUseActiveX()) { + Parser.prototype.parseFromString = function (string) { + var doc = new window.ActiveXObject('htmlfile'); + doc.designMode = 'on'; // disable on-page scripts + doc.open(); + doc.write(string); + doc.close(); + return doc + }; + } else { + Parser.prototype.parseFromString = function (string) { + var doc = document.implementation.createHTMLDocument(''); + doc.open(); + doc.write(string); + doc.close(); + return doc + }; + } + } + return Parser + } + + function shouldUseActiveX () { + var useActiveX = false; + try { + document.implementation.createHTMLDocument('').open(); + } catch (e) { + if (window.ActiveXObject) useActiveX = true; + } + return useActiveX + } + + var HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser(); + + function RootNode (input, options) { + var root; + if (typeof input === 'string') { + var doc = htmlParser().parseFromString( + // DOM parsers arrange elements in the and . + // Wrapping in a custom element ensures elements are reliably arranged in + // a single element. + '' + input + '', + 'text/html' + ); + root = doc.getElementById('turndown-root'); + } else { + root = input.cloneNode(true); + } + collapseWhitespace({ + element: root, + isBlock: isBlock, + isVoid: isVoid, + isPre: options.preformattedCode ? isPreOrCode : null + }); + + return root + } + + var _htmlParser; + function htmlParser () { + _htmlParser = _htmlParser || new HTMLParser(); + return _htmlParser + } + + function isPreOrCode (node) { + return node.nodeName === 'PRE' || node.nodeName === 'CODE' + } + + function Node (node, options) { + node.isBlock = isBlock(node); + node.isCode = node.nodeName === 'CODE' || node.parentNode.isCode; + node.isBlank = isBlank(node); + node.flankingWhitespace = flankingWhitespace(node, options); + return node + } + + function isBlank (node) { + return ( + !isVoid(node) && + !isMeaningfulWhenBlank(node) && + /^\s*$/i.test(node.textContent) && + !hasVoid(node) && + !hasMeaningfulWhenBlank(node) + ) + } + + function flankingWhitespace (node, options) { + if (node.isBlock || (options.preformattedCode && node.isCode)) { + return { leading: '', trailing: '' } + } + + var edges = edgeWhitespace(node.textContent); + + // abandon leading ASCII WS if left-flanked by ASCII WS + if (edges.leadingAscii && isFlankedByWhitespace('left', node, options)) { + edges.leading = edges.leadingNonAscii; + } + + // abandon trailing ASCII WS if right-flanked by ASCII WS + if (edges.trailingAscii && isFlankedByWhitespace('right', node, options)) { + edges.trailing = edges.trailingNonAscii; + } + + return { leading: edges.leading, trailing: edges.trailing } + } + + function edgeWhitespace (string) { + var m = string.match(/^(([ \t\r\n]*)(\s*))[\s\S]*?((\s*?)([ \t\r\n]*))$/); + return { + leading: m[1], // whole string for whitespace-only strings + leadingAscii: m[2], + leadingNonAscii: m[3], + trailing: m[4], // empty for whitespace-only strings + trailingNonAscii: m[5], + trailingAscii: m[6] + } + } + + function isFlankedByWhitespace (side, node, options) { + var sibling; + var regExp; + var isFlanked; + + if (side === 'left') { + sibling = node.previousSibling; + regExp = / $/; + } else { + sibling = node.nextSibling; + regExp = /^ /; + } + + if (sibling) { + if (sibling.nodeType === 3) { + isFlanked = regExp.test(sibling.nodeValue); + } else if (options.preformattedCode && sibling.nodeName === 'CODE') { + isFlanked = false; + } else if (sibling.nodeType === 1 && !isBlock(sibling)) { + isFlanked = regExp.test(sibling.textContent); + } + } + return isFlanked + } + + var reduce = Array.prototype.reduce; + var escapes = [ + [/\\/g, '\\\\'], + [/\*/g, '\\*'], + [/^-/g, '\\-'], + [/^\+ /g, '\\+ '], + [/^(=+)/g, '\\$1'], + [/^(#{1,6}) /g, '\\$1 '], + [/`/g, '\\`'], + [/^~~~/g, '\\~~~'], + [/\[/g, '\\['], + [/\]/g, '\\]'], + [/^>/g, '\\>'], + [/_/g, '\\_'], + [/^(\d+)\. /g, '$1\\. '] + ]; + + function TurndownService (options) { + if (!(this instanceof TurndownService)) return new TurndownService(options) + + var defaults = { + rules: rules, + headingStyle: 'setext', + hr: '* * *', + bulletListMarker: '*', + codeBlockStyle: 'indented', + fence: '```', + emDelimiter: '_', + strongDelimiter: '**', + linkStyle: 'inlined', + linkReferenceStyle: 'full', + br: ' ', + preformattedCode: false, + blankReplacement: function (content, node) { + return node.isBlock ? '\n\n' : '' + }, + keepReplacement: function (content, node) { + return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML + }, + defaultReplacement: function (content, node) { + return node.isBlock ? '\n\n' + content + '\n\n' : content + } + }; + this.options = extend({}, defaults, options); + this.rules = new Rules(this.options); + } + + TurndownService.prototype = { + /** + * The entry point for converting a string or DOM node to Markdown + * @public + * @param {String|HTMLElement} input The string or DOM node to convert + * @returns A Markdown representation of the input + * @type String + */ + + turndown: function (input) { + if (!canConvert(input)) { + throw new TypeError( + input + ' is not a string, or an element/document/fragment node.' + ) + } + + if (input === '') return '' + + var output = process.call(this, new RootNode(input, this.options)); + return postProcess.call(this, output) + }, + + /** + * Add one or more plugins + * @public + * @param {Function|Array} plugin The plugin or array of plugins to add + * @returns The Turndown instance for chaining + * @type Object + */ + + use: function (plugin) { + if (Array.isArray(plugin)) { + for (var i = 0; i < plugin.length; i++) this.use(plugin[i]); + } else if (typeof plugin === 'function') { + plugin(this); + } else { + throw new TypeError('plugin must be a Function or an Array of Functions') + } + return this + }, + + /** + * Adds a rule + * @public + * @param {String} key The unique key of the rule + * @param {Object} rule The rule + * @returns The Turndown instance for chaining + * @type Object + */ + + addRule: function (key, rule) { + this.rules.add(key, rule); + return this + }, + + /** + * Keep a node (as HTML) that matches the filter + * @public + * @param {String|Array|Function} filter The unique key of the rule + * @returns The Turndown instance for chaining + * @type Object + */ + + keep: function (filter) { + this.rules.keep(filter); + return this + }, + + /** + * Remove a node that matches the filter + * @public + * @param {String|Array|Function} filter The unique key of the rule + * @returns The Turndown instance for chaining + * @type Object + */ + + remove: function (filter) { + this.rules.remove(filter); + return this + }, + + /** + * Escapes Markdown syntax + * @public + * @param {String} string The string to escape + * @returns A string with Markdown syntax escaped + * @type String + */ + + escape: function (string) { + return escapes.reduce(function (accumulator, escape) { + return accumulator.replace(escape[0], escape[1]) + }, string) + } + }; + + /** + * Reduces a DOM node down to its Markdown string equivalent + * @private + * @param {HTMLElement} parentNode The node to convert + * @returns A Markdown representation of the node + * @type String + */ + + function process (parentNode) { + var self = this; + return reduce.call(parentNode.childNodes, function (output, node) { + node = new Node(node, self.options); + + var replacement = ''; + if (node.nodeType === 3) { + replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue); + } else if (node.nodeType === 1) { + replacement = replacementForNode.call(self, node); + } + + return join(output, replacement) + }, '') + } + + /** + * Appends strings as each rule requires and trims the output + * @private + * @param {String} output The conversion output + * @returns A trimmed version of the ouput + * @type String + */ + + function postProcess (output) { + var self = this; + this.rules.forEach(function (rule) { + if (typeof rule.append === 'function') { + output = join(output, rule.append(self.options)); + } + }); + + return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '') + } + + /** + * Converts an element node to its Markdown equivalent + * @private + * @param {HTMLElement} node The node to convert + * @returns A Markdown representation of the node + * @type String + */ + + function replacementForNode (node) { + var rule = this.rules.forNode(node); + var content = process.call(this, node); + var whitespace = node.flankingWhitespace; + if (whitespace.leading || whitespace.trailing) content = content.trim(); + return ( + whitespace.leading + + rule.replacement(content, node, this.options) + + whitespace.trailing + ) + } + + /** + * Joins replacement to the current output with appropriate number of new lines + * @private + * @param {String} output The current conversion output + * @param {String} replacement The string to append to the output + * @returns Joined output + * @type String + */ + + function join (output, replacement) { + var s1 = trimTrailingNewlines(output); + var s2 = trimLeadingNewlines(replacement); + var nls = Math.max(output.length - s1.length, replacement.length - s2.length); + var separator = '\n\n'.substring(0, nls); + + return s1 + separator + s2 + } + + /** + * Determines whether an input can be converted + * @private + * @param {String|HTMLElement} input Describe this parameter + * @returns Describe what it returns + * @type String|Object|Array|Boolean|Number + */ + + function canConvert (input) { + return ( + input != null && ( + typeof input === 'string' || + (input.nodeType && ( + input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11 + )) + ) + ) + } + + return TurndownService; + +}))); diff --git a/www/pad/export.js b/www/pad/export.js index f1e3497c9..01ac27d7e 100644 --- a/www/pad/export.js +++ b/www/pad/export.js @@ -1,12 +1,24 @@ define([ 'jquery', '/common/common-util.js', + '/common/diffMarked.js', + '/common/hyperscript.js', '/bower_components/hyperjson/hyperjson.js', '/bower_components/nthen/index.js', -], function ($, Util, Hyperjson, nThen) { + '/lib/turndown.browser.umd.js' +], function ($, Util, DiffMd, h, Hyperjson, nThen, Turndown) { var module = { ext: '.html', // default - exts: ['.html', '.doc'] + exts: ['.html', '.md', '.doc'] + }; + + module.importMd = function (md, common) { + var html = DiffMd.render(md, true, false, true); + var div = h('div#cp-temp'); + DiffMd.apply(html, $(div), common); + var body = h('body'); + body.innerHTML = div.innerHTML; + return body; }; var exportMediaTags = function (inner, cb) { @@ -77,6 +89,15 @@ define([ }); return void cb(blob); } + if (ext === ".md") { + var md = Turndown({ + headingStyle: 'atx' + }).turndown(toExport); + var mdBlob = new Blob([md], { + type: 'text/markdown;charset=utf-8' + }); + return void cb(mdBlob); + } var html = module.getHTML(toExport); cb(new Blob([ html ], { type: "text/html;charset=utf-8" })); }); diff --git a/www/pad/inner.js b/www/pad/inner.js index 47ab5dbe3..304e9abe4 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -1115,7 +1115,7 @@ define([ framework._.sfCommon.isPadStored(function(err, val) { if (!val) { return; } - var b64images = $inner.find('img[src^="data:image"]:not(.cke_reset)'); + var b64images = $inner.find('img[src^="data:image"]:not(.cke_reset), img[src^="data:application/octet-stream"]:not(.cke_reset)'); if (b64images.length && framework._.sfCommon.isLoggedIn()) { var no = h('button.cp-corner-cancel', Messages.cancel); var yes = h('button.cp-corner-primary', Messages.ok); @@ -1169,7 +1169,14 @@ define([ }); cb($dom[0]); }; - framework.setFileImporter({ accept: 'text/html' }, function(content, f, cb) { + framework.setFileImporter({ accept: ['.md', 'text/html'] }, function(content, f, cb) { + if (!f) { return; } + if (/\.md$/.test(f.name)) { + var mdDom = Exporter.importMd(content, framework._.sfCommon); + return importMediaTags(mdDom, function(dom) { + cb(Hyperjson.fromDOM(dom)); + }); + } importMediaTags(domFromHTML(content).body, function(dom) { cb(Hyperjson.fromDOM(dom)); }); @@ -1320,6 +1327,7 @@ define([ $(waitFor()); }).nThen(function(waitFor) { Ckeditor.config.toolbarCanCollapse = true; + Ckeditor.config.language = Messages._getLanguage(); if (screen.height < 800) { Ckeditor.config.toolbarStartupExpanded = false; $('meta[name=viewport]').attr('content', diff --git a/www/poll/inner.js b/www/poll/inner.js index 6d4e0590e..53faea7e9 100644 --- a/www/poll/inner.js +++ b/www/poll/inner.js @@ -955,7 +955,7 @@ define([ var fileHost = privateData.fileHost || privateData.origin; var src = fileHost + Hash.getBlobPathFromHex(secret.channel); var key = Hash.encodeBase64(secret.keys.cryptKey); - var mt = ''; + var mt = UI.mediaTag(src, key).outerHTML; APP.editor.replaceSelection(mt); } }; @@ -1235,7 +1235,7 @@ define([ common.openFilePicker(pickerCfg, function (data) { if (data.type === 'file' && APP.editor) { common.setPadAttribute('atime', +new Date(), null, data.href); - var mt = ''; + var mt = UI.mediaTag(data.src, data.key).outerHTML; APP.editor.replaceSelection(mt); return; } diff --git a/www/register/main.js b/www/register/main.js index 4c63e4482..4d9274253 100644 --- a/www/register/main.js +++ b/www/register/main.js @@ -47,7 +47,11 @@ define([ var I_REALLY_WANT_TO_USE_MY_EMAIL_FOR_MY_USERNAME = false; var registerClick = function () { - var uname = $uname.val(); + var uname = $uname.val().trim(); + // trim whitespace surrounding the username since it is otherwise included in key derivation + // most people won't realize that its presence is significant + $uname.val(uname); + var passwd = $passwd.val(); var confirmPassword = $confirm.val(); diff --git a/www/slide/app-slide.less b/www/slide/app-slide.less index 7e518a1c7..ce12b1de3 100644 --- a/www/slide/app-slide.less +++ b/www/slide/app-slide.less @@ -356,6 +356,7 @@ } .markdown_main(); + .markdown_cryptpad(); .markdown_preformatted-code; .markdown_gfm-table(); diff --git a/www/slide/inner.js b/www/slide/inner.js index 3eb1435ed..e40bd6d79 100644 --- a/www/slide/inner.js +++ b/www/slide/inner.js @@ -556,7 +556,7 @@ define([ var fileHost = privateData.fileHost || privateData.origin; var src = fileHost + Hash.getBlobPathFromHex(secret.channel); var key = Hash.encodeBase64(secret.keys.cryptKey); - var mt = ''; + var mt = UI.mediaTag(src, key).outerHTML; editor.replaceSelection(mt); } }; diff --git a/www/support/inner.js b/www/support/inner.js index f466e7693..6d9e5eae8 100644 --- a/www/support/inner.js +++ b/www/support/inner.js @@ -272,19 +272,27 @@ define([ APP.$rightside = $('
    ', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container); var sFrameChan = common.getSframeChannel(); sFrameChan.onReady(waitFor()); + }).nThen(function (waitFor) { + metadataMgr = common.getMetadataMgr(); + privateData = metadataMgr.getPrivateData(); common.getPinUsage(null, waitFor(function (err, data) { if (err) { return void console.error(err); } APP.pinUsage = data; })); + APP.teamsUsage = {}; + Object.keys(privateData.teams).forEach(function (teamId) { + common.getPinUsage(teamId, waitFor(function (err, data) { + if (err) { return void console.error(err); } + APP.teamsUsage[teamId] = data; + })); + }); }).nThen(function (/*waitFor*/) { createToolbar(); - metadataMgr = common.getMetadataMgr(); - privateData = metadataMgr.getPrivateData(); common.setTabTitle(Messages.supportPage); APP.origin = privateData.origin; APP.readOnly = privateData.readOnly; - APP.support = Support.create(common, false, APP.pinUsage); + APP.support = Support.create(common, false, APP.pinUsage, APP.teamsUsage); // Content var $rightside = APP.$rightside; diff --git a/www/support/ui.js b/www/support/ui.js index 679776c59..90fd43fa5 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -31,9 +31,7 @@ define([ if (typeof(ctx.pinUsage) === 'object') { // pass pin.usage, pin.limit, and pin.plan if supplied - Object.keys(ctx.pinUsage).forEach(function (k) { - data.sender[k] = ctx.pinUsage[k]; - }); + data.sender.quota = ctx.pinUsage; } data.id = id; @@ -45,11 +43,14 @@ define([ data.sender.blockLocation = privateData.blockLocation || ''; data.sender.teams = Object.keys(teams).map(function (key) { var team = teams[key]; - if (!teams) { return; } + if (!team) { return; } var ret = {}; - ['edPublic', 'owner', 'viewer', 'hasSecondaryKey', 'validKeys'].forEach(function (k) { + ['channel', 'roster', 'numberPads', 'numberSf', 'edPublic', 'curvePublic', 'owner', 'viewer', 'hasSecondaryKey', 'validKeys'].forEach(function (k) { ret[k] = team[k]; }); + if (ctx.teamsUsage && ctx.teamsUsage[key]) { + ret.quota = ctx.teamsUsage[key]; + } return ret; }).filter(Boolean); @@ -430,12 +431,13 @@ define([ ]); }; - var create = function (common, isAdmin, pinUsage) { + var create = function (common, isAdmin, pinUsage, teamsUsage) { var ui = {}; var ctx = { common: common, isAdmin: isAdmin, pinUsage: pinUsage || false, + teamsUsage: teamsUsage || false, adminKeys: Array.isArray(ApiConfig.adminKeys)? ApiConfig.adminKeys.slice(): [], }; diff --git a/www/teams/inner.js b/www/teams/inner.js index 4b0532628..373a27998 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -768,7 +768,7 @@ define([ $(demote).hide(); describeUser(common, data.curvePublic, { role: role - }, promote); + }, demote); }; if (isMe) { return void UI.confirm(Messages.team_demoteMeConfirm, function (yes) { @@ -901,22 +901,23 @@ define([ $header.append(invite); } - if (me && (me.role !== 'OWNER')) { - var leave = h('button.cp-online.btn.btn-danger', Messages.team_leaveButton); - $(leave).click(function () { - UI.confirm(Messages.team_leaveConfirm, function (yes) { - if (!yes) { return; } - APP.module.execCommand('LEAVE_TEAM', { - teamId: APP.team - }, function (obj) { - if (obj && obj.error) { - return void UI.warn(Messages.error); - } - }); + var leave = h('button.cp-online.btn.btn-danger', Messages.team_leaveButton); + $(leave).click(function () { + if (me && me.role === 'OWNER') { + return void UI.alert(Messages.team_leaveOwner); + } + UI.confirm(Messages.team_leaveConfirm, function (yes) { + if (!yes) { return; } + APP.module.execCommand('LEAVE_TEAM', { + teamId: APP.team + }, function (obj) { + if (obj && obj.error) { + return void UI.warn(Messages.error); + } }); }); - $header.append(leave); - } + }); + $header.append(leave); var table = h('button.btn.btn-primary', Messages.teams_table); $(table).click(function (e) {